rrubocop 0.8.16

10x faster RuboCop drop-in. Same output format and configs, without maintaining RuboCop gems.
//! Redundant `&block` / `&blk` / `&proc` forwarded as anonymous `&`.

use tree_sitter::Node;

use crate::cop::shared::{
    EXCLUDED_BLOCK_PARENTS, METHOD_STOPS, NESTED_SCOPES, ScanOpts, replace_arg, scan,
};
use crate::cop::{Cop, CopConfig};
use crate::correction::Correction;
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;

use super::{ArgumentsForwarding, COP, method_body, param_name};

const BLOCK_MSG: &str = "Use anonymous block arguments forwarding (`&`).";

pub(super) fn report(
    cop: &ArgumentsForwarding,
    source: &SourceFile,
    method: Node<'_>,
    params: Node<'_>,
    config: &CopConfig,
    ruby: f64,
    diagnostics: &mut Vec<Diagnostic>,
    corrections: &mut Option<&mut Vec<Correction>>,
) {
    let Some((name, param, body)) = candidate(source, method, params, config, ruby) else {
        return;
    };
    let found = scan(
        source,
        body,
        name,
        Some(name),
        &ScanOpts {
            skip_scopes: NESTED_SCOPES,
            excluded_parents: EXCLUDED_BLOCK_PARENTS,
            scope_stops: METHOD_STOPS,
            include_lambda: true,
            any_inside_block: false,
            guard_inside_block: ruby < 3.4,
        },
    );
    if found.name_used || found.blocked || found.nodes.is_empty() {
        return;
    }
    for pass in found.nodes {
        report_block(cop, source, pass, diagnostics, corrections);
    }
    report_block(cop, source, param, diagnostics, corrections);
}

fn candidate<'a>(
    source: &'a SourceFile,
    method: Node<'a>,
    params: Node<'a>,
    config: &CopConfig,
    ruby: f64,
) -> Option<(&'a [u8], Node<'a>, Node<'a>)> {
    if ruby < 3.1 || (ruby >= 3.2 && !config.get_bool("UseAnonymousForwarding", true)) {
        return None;
    }
    let param = block_param(params)?;
    let name = param_name(source, param)?;
    if !redundant_name(config, name) {
        return None;
    }
    let body = method_body(method)?;
    Some((name, param, body))
}

fn redundant_name(config: &CopConfig, name: &[u8]) -> bool {
    let Ok(name) = std::str::from_utf8(name) else {
        return false;
    };
    match config.options.get("RedundantBlockArgumentNames") {
        Some(serde_yml::Value::Sequence(items)) => items.iter().any(|v| v.as_str() == Some(name)),
        _ => matches!(name, "blk" | "block" | "proc"),
    }
}

fn block_param(params: Node<'_>) -> Option<Node<'_>> {
    let mut cur = params.walk();
    params
        .named_children(&mut cur)
        .find(|n| n.kind() == "block_parameter")
}

fn report_block(
    cop: &ArgumentsForwarding,
    source: &SourceFile,
    node: Node<'_>,
    diagnostics: &mut Vec<Diagnostic>,
    corrections: &mut Option<&mut Vec<Correction>>,
) {
    let (line, col) = source.offset_to_line_col(node.start_byte());
    let mut diag = cop.diagnostic(source, line, col, BLOCK_MSG.to_string());
    diag.highlight_length = node.end_byte().saturating_sub(node.start_byte()).max(1);
    if replace_arg(source, node, "&", corrections, COP) {
        diag.corrected = true;
    }
    diagnostics.push(diag);
}