rrubocop 0.8.15

10x faster RuboCop drop-in. Same output format and configs, without maintaining RuboCop gems.
//! Layout/SpaceBeforeFirstArg.

use tree_sitter::Node;

use crate::cop::layout::report;
use crate::cop::{Cop, CopConfig};
use crate::correction::Correction;
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;

pub struct SpaceBeforeFirstArg;

fn method_node(node: Node<'_>) -> Option<Node<'_>> {
    node.child_by_field_name("method")
        .or_else(|| node.child_by_field_name("name"))
}

fn first_arg(args: Node<'_>) -> Option<Node<'_>> {
    let mut cur = args.walk();
    args.named_children(&mut cur).next()
}

fn gap_spaces(bytes: &[u8], method_end: usize, first_start: usize) -> Option<usize> {
    let between = &bytes[method_end..first_start];
    if between.iter().any(|&b| b == b'\n') {
        return None;
    }
    Some(between.iter().filter(|&&b| b == b' ' || b == b'\t').count())
}

fn line_has_nonspace_at(line: &[u8], col: usize) -> bool {
    line.get(col).is_some_and(|b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
}

fn is_full_line_comment(line: &[u8]) -> bool {
    let mut i = 0;
    while i < line.len() && matches!(line[i], b' ' | b'\t') {
        i += 1;
    }
    line.get(i) == Some(&b'#')
}

/// RuboCop `AllowForAlignment` / `aligned_words?`: arg column matches a word
/// start on another (non-comment) line.
fn aligned_with_adjacent(source: &SourceFile, first_start: usize) -> bool {
    let (line, col) = source.offset_to_line_col(first_start);
    if line == 0 || col == 0 {
        return false;
    }
    let lines: Vec<&[u8]> = source.lines().collect();
    let idx = line - 1;
    lines.iter().enumerate().any(|(other, line_bytes)| {
        if other == idx || is_full_line_comment(line_bytes) {
            return false;
        }
        // `\s\S` at `col-1` / `col` — word begins at the argument column.
        matches!(line_bytes.get(col - 1), Some(b' ') | Some(b'\t'))
            && line_has_nonspace_at(line_bytes, col)
    })
}

fn offense(source: &SourceFile, node: Node<'_>, allow_align: bool) -> Option<(usize, usize)> {
    let (method_end, first_start) = unparen_call_gap(source, node)?;
    let spaces = gap_spaces(source.as_bytes(), method_end, first_start)?;
    bad_space_gap(source, method_end, first_start, spaces, allow_align)
}

fn unparen_call_gap(source: &SourceFile, node: Node<'_>) -> Option<(usize, usize)> {
    let bytes = source.as_bytes();
    let method = method_node(node)?;
    // `discount:` in a hash/pattern is a label, not `discount` + argument `:`.
    if bytes.get(method.end_byte()) == Some(&b':') {
        return None;
    }
    let args = node.child_by_field_name("arguments")?;
    if bytes.get(args.start_byte()) == Some(&b'(') {
        return None;
    }
    let first = first_arg(args)?;
    Some((method.end_byte(), first.start_byte()))
}

fn bad_space_gap(
    source: &SourceFile,
    method_end: usize,
    first_start: usize,
    spaces: usize,
    allow_align: bool,
) -> Option<(usize, usize)> {
    if spaces == 1 {
        return None;
    }
    if spaces > 1 && allow_align && aligned_with_adjacent(source, first_start) {
        return None;
    }
    Some((method_end, first_start))
}

impl Cop for SpaceBeforeFirstArg {
    fn name(&self) -> &'static str {
        "Layout/SpaceBeforeFirstArg"
    }
    fn supports_autocorrect(&self) -> bool {
        true
    }
    fn interested_node_kinds(&self) -> &'static [&'static str] {
        &["call", "command", "command_call"]
    }

    fn check_node(
        &self,
        source: &SourceFile,
        node: Node<'_>,
        config: &CopConfig,
        diagnostics: &mut Vec<Diagnostic>,
        mut corrections: Option<&mut Vec<Correction>>,
    ) {
        let Some((start, end)) =
            offense(source, node, config.get_bool("AllowForAlignment", true))
        else {
            return;
        };
        report::report_fix(
            self,
            source,
            start,
            "Put one space between the method name and the first argument.".into(),
            diagnostics,
            &mut corrections,
            start,
            end,
            " ".into(),
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    crate::cop_fixture_tests!(SpaceBeforeFirstArg, "cops/layout/space_before_first_arg");
}