rrubocop 0.8.15

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

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 SpaceInsideArrayPercentLiteral;

fn is_array_percent(text: &[u8]) -> bool {
    text.starts_with(b"%w")
        || text.starts_with(b"%W")
        || text.starts_with(b"%i")
        || text.starts_with(b"%I")
}

fn matching_close(open: u8) -> u8 {
    match open {
        b'(' => b')',
        b'[' => b']',
        b'{' => b'}',
        b'<' => b'>',
        c => c,
    }
}

fn percent_span(text: &[u8]) -> Option<(usize, usize)> {
    let open_idx = 2 + text[2..].iter().position(|&b| !b.is_ascii_alphanumeric()).unwrap_or(0);
    if open_idx >= text.len() {
        return None;
    }
    let close = matching_close(text[open_idx]);
    let crel = text.iter().rposition(|&b| b == close)?;
    Some((open_idx, crel))
}

fn double_space_at(inner: &[u8]) -> Option<usize> {
    inner.windows(2).position(|w| w == b"  ")
}

fn collapse_spaces(bytes: &[u8], start: usize, limit: usize) -> usize {
    let mut e = start;
    while e < limit && bytes[e] == b' ' {
        e += 1;
    }
    e
}

fn report_double(
    cop: &dyn Cop,
    source: &SourceFile,
    bytes: &[u8],
    abs: usize,
    limit: usize,
    diagnostics: &mut Vec<Diagnostic>,
    corrections: &mut Option<&mut Vec<Correction>>,
) {
    report::report_fix(
        cop,
        source,
        abs,
        "Use only a single space inside array percent literal.".into(),
        diagnostics,
        corrections,
        abs,
        collapse_spaces(bytes, abs, limit),
        " ".into(),
    );
}

fn singleline_percent_inner(text: &[u8]) -> Option<(usize, usize, &[u8])> {
    let (open_idx, crel) = percent_span(text)?;
    let inner = &text[open_idx + 1..crel];
    if inner.contains(&b'\n') {
        return None;
    }
    Some((open_idx, crel, inner))
}

fn double_space_abs(source: &SourceFile, node: Node<'_>) -> Option<(usize, usize)> {
    let text = &source.as_bytes()[node.start_byte()..node.end_byte()];
    if !is_array_percent(text) {
        return None;
    }
    let (open_idx, crel, inner) = singleline_percent_inner(text)?;
    let rel = double_space_at(inner)?;
    Some((node.start_byte() + open_idx + 1 + rel, node.start_byte() + crel))
}

impl Cop for SpaceInsideArrayPercentLiteral {
    fn name(&self) -> &'static str {
        "Layout/SpaceInsideArrayPercentLiteral"
    }
    fn supports_autocorrect(&self) -> bool {
        true
    }
    fn interested_node_kinds(&self) -> &'static [&'static str] {
        &["string_array", "symbol_array", "%w", "%W", "%i", "%I"]
    }

    fn check_node(
        &self,
        source: &SourceFile,
        node: Node<'_>,
        config: &CopConfig,
        diagnostics: &mut Vec<Diagnostic>,
        mut corrections: Option<&mut Vec<Correction>>,
    ) {
        let _ = config;
        let Some((abs, limit)) = double_space_abs(source, node) else {
            return;
        };
        report_double(
            self,
            source,
            source.as_bytes(),
            abs,
            limit,
            diagnostics,
            &mut corrections,
        );
    }
}

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

    #[test]
    fn no_offense_multiline_fixture() {
        crate::testutil::assert_cop_no_offenses_full(
            &SpaceInsideArrayPercentLiteral,
            include_bytes!("../../../tests/fixtures/cops/layout/space_inside_array_percent_literal/no_offense_multiline.rb"),
        );
    }
}