use tree_sitter::Node;
use crate::cop::shared::{argument_nodes, node_bytes};
use crate::cop::{Cop, CopConfig};
use crate::correction::Correction;
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;
pub struct ArgumentsForwarding;
pub(super) const COP: &str = "Style/ArgumentsForwarding";
mod anon;
pub(super) fn param_name<'a>(source: &'a SourceFile, node: Node<'a>) -> Option<&'a [u8]> {
node.child_by_field_name("name")
.map(|n| node_bytes(source, n))
.or_else(|| {
let mut cur = node.walk();
node.named_children(&mut cur)
.find(|n| n.kind() == "identifier")
.map(|n| node_bytes(source, n))
})
}
fn rest_and_block<'a>(source: &'a SourceFile, params: Node<'a>) -> Option<(&'a [u8], &'a [u8])> {
let mut rest = None;
let mut block = None;
let mut cur = params.walk();
for child in params.named_children(&mut cur) {
match child.kind() {
"splat_parameter" => rest = param_name(source, child),
"block_parameter" => block = param_name(source, child),
_ => {}
}
}
Some((rest?, block?))
}
pub(super) fn arg_ident<'a>(source: &'a SourceFile, node: Node<'a>) -> Option<&'a [u8]> {
let mut cur = node.walk();
node.named_children(&mut cur)
.find(|n| n.kind() == "identifier")
.map(|n| node_bytes(source, n))
}
fn forwards_call(source: &SourceFile, node: Node<'_>, rest: &[u8], block: &[u8]) -> bool {
if !matches!(node.kind(), "call" | "command" | "command_call") {
return false;
}
let args = argument_nodes(node);
let mut has_rest = false;
let mut has_block = false;
for arg in &args {
match arg.kind() {
"splat_argument" if arg_ident(source, *arg) == Some(rest) => has_rest = true,
"block_argument" if arg_ident(source, *arg) == Some(block) => has_block = true,
_ => {}
}
}
has_rest && has_block
}
fn nested_scope(node: Node<'_>) -> bool {
matches!(
node.kind(),
"method" | "singleton_method" | "class" | "module" | "singleton_class"
)
}
fn params_used_outside(
source: &SourceFile,
body: Node<'_>,
call: Node<'_>,
rest: &[u8],
block: &[u8],
) -> bool {
let start = call.start_byte();
let end = call.end_byte();
let mut stack = vec![body];
while let Some(node) = stack.pop() {
if nested_scope(node) {
continue;
}
if param_ident_outside(source, node, rest, block, start, end) {
return true;
}
let mut cur = node.walk();
stack.extend(node.named_children(&mut cur));
}
false
}
fn param_ident_outside(
source: &SourceFile,
node: Node<'_>,
rest: &[u8],
block: &[u8],
start: usize,
end: usize,
) -> bool {
if node.kind() != "identifier"
|| (start..end).contains(&node.start_byte())
|| method_ident(node)
{
return false;
}
let text = node_bytes(source, node);
text == rest || text == block
}
fn method_ident(node: Node<'_>) -> bool {
node.parent()
.and_then(|parent| parent.child_by_field_name("method"))
.is_some_and(|method| method.id() == node.id())
}
pub(super) fn method_body(node: Node<'_>) -> Option<Node<'_>> {
node.child_by_field_name("body").or_else(|| {
let mut cur = node.walk();
node.named_children(&mut cur)
.find(|n| n.kind() == "body_statement")
})
}
fn find_forwarding_call(
source: &SourceFile,
body: Node<'_>,
rest: &[u8],
block: &[u8],
) -> Option<usize> {
let mut stack = vec![body];
while let Some(node) = stack.pop() {
if nested_scope(node) {
continue;
}
if forwards_call(source, node, rest, block)
&& !params_used_outside(source, body, node, rest, block)
{
return Some(node.start_byte());
}
let mut cur = node.walk();
stack.extend(node.named_children(&mut cur));
}
None
}
impl Cop for ArgumentsForwarding {
fn name(&self) -> &'static str {
COP
}
fn supports_autocorrect(&self) -> bool {
true
}
fn interested_node_kinds(&self) -> &'static [&'static str] {
&["method", "singleton_method"]
}
fn check_node(
&self,
source: &SourceFile,
node: Node<'_>,
config: &CopConfig,
diagnostics: &mut Vec<Diagnostic>,
mut corrections: Option<&mut Vec<Correction>>,
) {
let ruby = config.get_f64("TargetRubyVersion", 2.7);
if ruby < 2.7 {
return;
}
let Some(params) = node.child_by_field_name("parameters") else {
return;
};
if config.get_bool("AllowOnlyRestArgument", true)
&& let Some((rest, block)) = rest_and_block(source, params)
&& let Some(body) = method_body(node)
&& let Some(off) = find_forwarding_call(source, body, rest, block)
{
let (line, col) = source.offset_to_line_col(off);
let mut diag =
self.diagnostic(source, line, col, "Use arguments forwarding.".to_string());
diag.correctable = false;
diagnostics.push(diag);
return;
}
anon::report(
self,
source,
node,
params,
config,
ruby,
diagnostics,
&mut corrections,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
crate::cop_fixture_tests!(ArgumentsForwarding, "cops/style/arguments_forwarding");
fn ruby34() -> CopConfig {
let mut config = CopConfig::default();
config.options.insert(
"TargetRubyVersion".into(),
serde_yml::Value::Number(serde_yml::Number::from(3.4)),
);
config
}
const LOADER: &str = "\
def load_variants(group_type, &block)
return unless applicable?
measure(operation:, group_type:, &block)
rescue StandardError
raise
end
";
fn collect(
src: &str,
) -> (
crate::parse::source::SourceFile,
Vec<crate::correction::Correction>,
) {
let source = crate::parse::source::SourceFile::from_bytes("t.rb", src.as_bytes().to_vec());
let tree = crate::parse::parse_ruby(&source).expect("parse");
let config = ruby34();
let mut corrections = Vec::new();
crate::cop::walker::BatchedWalker::new(vec![&ArgumentsForwarding], vec![&config]).walk(
&source,
tree.root_node(),
&mut Vec::new(),
Some(&mut corrections),
);
(source, corrections)
}
fn corrected(src: &str) -> String {
let (source, corrections) = collect(src);
String::from_utf8(
crate::correction::CorrectionSet::from_vec(corrections).apply(source.as_bytes()),
)
.expect("utf8")
}
#[test]
fn anonymous_block_is_correctable() {
let diags = crate::testutil::run_cop_full_with_config(
&ArgumentsForwarding,
LOADER.as_bytes(),
ruby34(),
);
assert_eq!(diags.len(), 2);
assert!(diags.iter().all(|d| {
d.correctable
&& d.highlight_length == 6
&& d.message == "Use anonymous block arguments forwarding (`&`)."
}));
}
#[test]
fn anonymous_block_autocorrect() {
assert_eq!(
corrected(LOADER),
"\
def load_variants(group_type, &)
return unless applicable?
measure(operation:, group_type:, &)
rescue StandardError
raise
end
"
);
}
#[test]
fn meaningful_block_name_is_clean() {
let diags = crate::testutil::run_cop_full_with_config(
&ArgumentsForwarding,
b"def foo(&callback)\n bar(&callback)\nend\ndef foo(&block)\n block.call\nend\n",
ruby34(),
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn nested_def_does_not_use_the_outer_block() {
let diags = crate::testutil::run_cop_full_with_config(
&ArgumentsForwarding,
b"def foo(&block)\n def inner\n block\n end\n bar(&block)\nend\n",
ruby34(),
);
assert_eq!(diags.len(), 2, "{diags:?}");
}
}