use ruby_prism::Node;
pub fn byte_offset_to_line(newline_positions: &[usize], byte_offset: usize) -> usize {
match newline_positions.binary_search(&byte_offset) {
Ok(idx) => idx + 1,
Err(idx) => idx + 1,
}
}
pub fn receiver_is_call_with_name(recv: &Option<Node<'_>>, name: &[u8]) -> bool {
match recv {
Some(node) => {
if let Some(call) = node.as_call_node() {
call.name().as_slice() == name
} else {
false
}
}
None => false,
}
}
pub fn receiver_as_call<'pr>(recv: &'pr Option<Node<'pr>>) -> Option<ruby_prism::CallNode<'pr>> {
match recv {
Some(node) => node.as_call_node(),
None => None,
}
}
pub fn has_block_pass(call: &ruby_prism::CallNode<'_>) -> bool {
matches!(call.block(), Some(Node::BlockArgumentNode { .. }))
}
pub fn has_full_block(call: &ruby_prism::CallNode<'_>) -> bool {
matches!(call.block(), Some(Node::BlockNode { .. }))
}
pub fn arg_count(call: &ruby_prism::CallNode<'_>) -> usize {
match call.arguments() {
Some(args) => args.arguments().iter().count(),
None => 0,
}
}
pub fn first_call_arg<'pr>(call: &ruby_prism::CallNode<'pr>) -> Option<Node<'pr>> {
call.arguments()
.and_then(|args| args.arguments().iter().next())
}
pub fn call_args_pair<'pr>(call: &ruby_prism::CallNode<'pr>) -> Option<(Node<'pr>, Node<'pr>)> {
let args = call.arguments()?;
let mut iter = args.arguments().iter();
let first = iter.next()?;
let second = iter.next()?;
if iter.next().is_some() {
return None; }
Some((first, second))
}
pub fn is_single_char_string(node: &Node<'_>) -> bool {
match node.as_string_node() {
Some(s) => s.unescaped().len() == 1,
None => false,
}
}
pub fn receiver_is_range(recv: &Option<Node<'_>>) -> bool {
match recv {
Some(node) => {
if node.as_range_node().is_some() {
return true;
}
if let Some(paren) = node.as_parentheses_node()
&& let Some(body) = paren.body()
{
if let Some(stmts) = body.as_statements_node() {
let body_nodes: Vec<_> = stmts.body().iter().collect();
return body_nodes.len() == 1 && body_nodes[0].as_range_node().is_some();
}
return body.as_range_node().is_some();
}
false
}
None => false,
}
}
pub fn is_primitive(node: &Node<'_>) -> bool {
matches!(
node,
Node::IntegerNode { .. }
| Node::FloatNode { .. }
| Node::StringNode { .. }
| Node::SymbolNode { .. }
| Node::TrueNode { .. }
| Node::FalseNode { .. }
| Node::NilNode { .. }
| Node::ArrayNode { .. }
| Node::HashNode { .. }
| Node::RangeNode { .. }
| Node::RationalNode { .. }
| Node::ImaginaryNode { .. }
)
}
pub fn first_arg_is_single_pair_hash(call: &ruby_prism::CallNode<'_>) -> bool {
match first_call_arg(call) {
Some(node) => {
if let Some(h) = node.as_hash_node() {
return h.elements().iter().count() == 1;
}
if let Some(k) = node.as_keyword_hash_node() {
return k.elements().iter().count() == 1;
}
false
}
None => false,
}
}
pub fn is_int_one(node: &Node<'_>) -> bool {
if let Some(i) = node.as_integer_node() {
let text = i.location().as_slice();
matches!(
text,
b"1" | b"0x1" | b"0X1" | b"0b1" | b"0B1" | b"0o1" | b"0O1"
)
} else {
false
}
}
pub fn block_arg_names(params: &Option<Node<'_>>) -> Vec<String> {
match params {
Some(node) => {
if let Some(block_params) = node.as_block_parameters_node()
&& let Some(inner_params) = block_params.parameters()
{
return inner_params
.requireds()
.iter()
.filter_map(|p| {
p.as_required_parameter_node()
.map(|rp| String::from_utf8_lossy(rp.name().as_slice()).to_string())
})
.collect();
}
Vec::new()
}
None => Vec::new(),
}
}
pub fn def_block_arg_name(def: &ruby_prism::DefNode<'_>) -> Option<String> {
let params = def.parameters()?;
let block_param = params.block()?;
let name = block_param.name()?;
Some(String::from_utf8_lossy(name.as_slice()).to_string())
}
pub fn def_regular_arg_count(def: &ruby_prism::DefNode<'_>) -> usize {
match def.parameters() {
Some(params) => params.requireds().iter().count(),
None => 0,
}
}
pub fn def_first_arg_name(def: &ruby_prism::DefNode<'_>) -> Option<String> {
let params = def.parameters()?;
let first = params.requireds().iter().next()?;
first
.as_required_parameter_node()
.map(|rp| String::from_utf8_lossy(rp.name().as_slice()).to_string())
}
pub fn str_contains_def(node: &Node<'_>) -> bool {
if let Some(s) = node.as_string_node() {
return String::from_utf8_lossy(s.unescaped()).contains("def");
}
if let Some(interp) = node.as_interpolated_string_node() {
return interp.parts().iter().any(|part| {
if let Some(s) = part.as_string_node() {
String::from_utf8_lossy(s.unescaped()).contains("def")
} else {
false
}
});
}
false
}
pub fn body_expression_count(body: &Option<Node<'_>>) -> usize {
match body {
None => 0,
Some(node) => {
if let Some(stmts) = node.as_statements_node() {
stmts.body().iter().count()
} else {
1
}
}
}
}
pub fn body_single_expression(body: Option<Node<'_>>) -> Option<Node<'_>> {
let node = body?;
if let Some(stmts) = node.as_statements_node() {
let mut iter = stmts.body().iter();
let first = iter.next()?;
if iter.next().is_some() {
return None; }
Some(first)
} else {
Some(node)
}
}
#[cfg(test)]
pub mod test_helpers {
use ruby_prism::{Node, ParseResult};
pub fn leak_parse(source: &[u8]) -> &'static ParseResult<'static> {
let owned: Vec<u8> = source.to_vec();
let static_source: &'static [u8] = Box::leak(owned.into_boxed_slice());
Box::leak(Box::new(ruby_prism::parse(static_source)))
}
pub fn parse_first_stmt(source: &[u8]) -> Node<'static> {
let result = leak_parse(source);
let program = result.node();
let prog = program.as_program_node().unwrap();
prog.statements().body().iter().next().unwrap()
}
}
pub fn compute_newline_positions(source: &[u8]) -> Vec<usize> {
source
.iter()
.enumerate()
.filter(|&(_, &b)| b == b'\n')
.map(|(i, _)| i)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast_helpers::test_helpers::parse_first_stmt;
#[test]
fn byte_offset_to_line_basic() {
let positions = vec![5, 11];
assert_eq!(byte_offset_to_line(&positions, 0), 1);
assert_eq!(byte_offset_to_line(&positions, 5), 1); assert_eq!(byte_offset_to_line(&positions, 6), 2);
assert_eq!(byte_offset_to_line(&positions, 12), 3);
}
#[test]
fn byte_offset_to_line_empty() {
assert_eq!(byte_offset_to_line(&[], 0), 1);
assert_eq!(byte_offset_to_line(&[], 100), 1);
}
#[test]
fn receiver_is_call_with_name_works() {
let node = parse_first_stmt(b"a.foo.bar");
let call = node.as_call_node().unwrap();
assert!(receiver_is_call_with_name(&call.receiver(), b"foo"));
assert!(!receiver_is_call_with_name(&call.receiver(), b"baz"));
}
#[test]
fn receiver_is_call_with_name_none() {
assert!(!receiver_is_call_with_name(&None, b"foo"));
}
#[test]
fn receiver_as_call_works() {
let node = parse_first_stmt(b"a.foo.bar");
let call = node.as_call_node().unwrap();
let recv = call.receiver();
let inner = receiver_as_call(&recv).unwrap();
assert_eq!(inner.name().as_slice(), b"foo");
}
#[test]
fn receiver_as_call_not_call() {
assert!(receiver_as_call(&None).is_none());
}
#[test]
fn has_block_pass_works() {
let node = parse_first_stmt(b"arr.map(&:to_s)");
let call = node.as_call_node().unwrap();
assert!(has_block_pass(&call));
}
#[test]
fn has_block_pass_without() {
let node = parse_first_stmt(b"arr.map(1)");
let call = node.as_call_node().unwrap();
assert!(!has_block_pass(&call));
}
#[test]
fn arg_count_works() {
let node = parse_first_stmt(b"arr.select(1, 2)");
let call = node.as_call_node().unwrap();
assert_eq!(arg_count(&call), 2);
}
#[test]
fn is_single_char_string_works() {
let node = parse_first_stmt(b"'x'");
assert!(is_single_char_string(&node));
let node2 = parse_first_stmt(b"'xy'");
assert!(!is_single_char_string(&node2));
}
#[test]
fn is_single_char_string_not_string() {
let node = parse_first_stmt(b"42");
assert!(!is_single_char_string(&node));
}
#[test]
fn receiver_is_range_inclusive() {
let node = parse_first_stmt(b"(1..10).include?(5)");
let call = node.as_call_node().unwrap();
assert!(receiver_is_range(&call.receiver()));
}
#[test]
fn receiver_is_range_exclusive() {
let node = parse_first_stmt(b"(1...10).include?(5)");
let call = node.as_call_node().unwrap();
assert!(receiver_is_range(&call.receiver()));
}
#[test]
fn receiver_is_range_not_range() {
let node = parse_first_stmt(b"[1].include?(5)");
let call = node.as_call_node().unwrap();
assert!(!receiver_is_range(&call.receiver()));
}
#[test]
fn is_primitive_covers_types() {
assert!(is_primitive(&parse_first_stmt(b"42")));
assert!(is_primitive(&parse_first_stmt(b"3.14")));
assert!(is_primitive(&parse_first_stmt(b"'s'")));
assert!(is_primitive(&parse_first_stmt(b":sym")));
assert!(is_primitive(&parse_first_stmt(b"true")));
assert!(is_primitive(&parse_first_stmt(b"false")));
assert!(is_primitive(&parse_first_stmt(b"nil")));
assert!(is_primitive(&parse_first_stmt(b"[]")));
assert!(is_primitive(&parse_first_stmt(b"{}")));
assert!(is_primitive(&parse_first_stmt(b"1..5")));
assert!(is_primitive(&parse_first_stmt(b"1...5")));
assert!(!is_primitive(&parse_first_stmt(b"x")));
}
#[test]
fn first_arg_is_single_pair_hash_kwargs() {
let node = parse_first_stmt(b"h.merge!(a: 1)");
let call = node.as_call_node().unwrap();
assert!(first_arg_is_single_pair_hash(&call));
}
#[test]
fn first_arg_is_single_pair_hash_explicit() {
let node = parse_first_stmt(b"h.merge!({a: 1})");
let call = node.as_call_node().unwrap();
assert!(first_arg_is_single_pair_hash(&call));
}
#[test]
fn first_arg_is_single_pair_hash_multi() {
let node = parse_first_stmt(b"h.merge!(a: 1, b: 2)");
let call = node.as_call_node().unwrap();
assert!(!first_arg_is_single_pair_hash(&call));
}
#[test]
fn first_arg_is_single_pair_hash_not_hash() {
let node = parse_first_stmt(b"h.merge!(x)");
let call = node.as_call_node().unwrap();
assert!(!first_arg_is_single_pair_hash(&call));
}
#[test]
fn is_int_one_works() {
assert!(is_int_one(&parse_first_stmt(b"1")));
assert!(!is_int_one(&parse_first_stmt(b"2")));
assert!(!is_int_one(&parse_first_stmt(b"'1'")));
}
#[test]
fn block_arg_names_single() {
let node = parse_first_stmt(b"arr.map { |x| x }");
let call = node.as_call_node().unwrap();
if let Some(Node::BlockNode { .. }) = call.block() {
let block = call.block().unwrap().as_block_node().unwrap();
let names = block_arg_names(&block.parameters());
assert_eq!(names, vec!["x".to_string()]);
} else {
panic!("Expected BlockNode");
}
}
#[test]
fn block_arg_names_none() {
let names = block_arg_names(&None);
assert!(names.is_empty());
}
#[test]
fn def_block_arg_name_present() {
let node = parse_first_stmt(b"def foo(&block); end");
let def = node.as_def_node().unwrap();
assert_eq!(def_block_arg_name(&def), Some("block".to_string()));
}
#[test]
fn def_block_arg_name_absent() {
let node = parse_first_stmt(b"def foo(x); end");
let def = node.as_def_node().unwrap();
assert_eq!(def_block_arg_name(&def), None);
}
#[test]
fn def_regular_arg_count_works() {
let node = parse_first_stmt(b"def foo(a, b); end");
let def = node.as_def_node().unwrap();
assert_eq!(def_regular_arg_count(&def), 2);
}
#[test]
fn def_regular_arg_count_no_args() {
let node = parse_first_stmt(b"def foo; end");
let def = node.as_def_node().unwrap();
assert_eq!(def_regular_arg_count(&def), 0);
}
#[test]
fn def_first_arg_name_works() {
let node = parse_first_stmt(b"def foo(bar); end");
let def = node.as_def_node().unwrap();
assert_eq!(def_first_arg_name(&def), Some("bar".to_string()));
}
#[test]
fn def_first_arg_name_no_args() {
let node = parse_first_stmt(b"def foo; end");
let def = node.as_def_node().unwrap();
assert_eq!(def_first_arg_name(&def), None);
}
#[test]
fn str_contains_def_in_string() {
let node = parse_first_stmt(b"\"def foo\"");
assert!(str_contains_def(&node));
}
#[test]
fn str_contains_def_no_def() {
let node = parse_first_stmt(b"\"hello\"");
assert!(!str_contains_def(&node));
}
#[test]
fn str_contains_def_not_string() {
let node = parse_first_stmt(b"42");
assert!(!str_contains_def(&node));
}
#[test]
fn str_contains_def_heredoc() {
let node = parse_first_stmt(b"<<~RUBY\ndef foo\nRUBY\n");
assert!(str_contains_def(&node));
}
#[test]
fn body_expression_count_none() {
assert_eq!(body_expression_count(&None), 0);
}
#[test]
fn body_expression_count_single() {
let node = parse_first_stmt(b"def foo; 42; end");
let def = node.as_def_node().unwrap();
assert_eq!(body_expression_count(&def.body()), 1);
}
#[test]
fn body_expression_count_multiple() {
let node = parse_first_stmt(b"def foo; 1; 2; 3; end");
let def = node.as_def_node().unwrap();
assert_eq!(body_expression_count(&def.body()), 3);
}
#[test]
fn body_single_expression_works() {
let node = parse_first_stmt(b"def foo; 42; end");
let def = node.as_def_node().unwrap();
let single = body_single_expression(def.body());
assert!(single.is_some());
assert!(single.unwrap().as_integer_node().is_some());
}
#[test]
fn body_single_expression_none_for_multiple() {
let node = parse_first_stmt(b"def foo; 1; 2; end");
let def = node.as_def_node().unwrap();
assert!(body_single_expression(def.body()).is_none());
}
#[test]
fn body_single_expression_none_for_empty() {
assert!(body_single_expression(None).is_none());
}
#[test]
fn compute_newline_positions_works() {
let source = b"line1\nline2\nline3";
let positions = compute_newline_positions(source);
assert_eq!(positions, vec![5, 11]);
}
#[test]
fn compute_newline_positions_empty() {
assert!(compute_newline_positions(b"").is_empty());
}
#[test]
fn compute_newline_positions_no_newlines() {
assert!(compute_newline_positions(b"hello").is_empty());
}
#[test]
fn prism_handles_ascii_encoding() {
let source = b"# encoding: ASCII\nx = 1\n";
let result = ruby_prism::parse(source);
assert!(result.errors().next().is_none());
}
#[test]
fn prism_handles_us_ascii_encoding() {
let source = b"# encoding: us-ascii\nx = 1\n";
let result = ruby_prism::parse(source);
assert!(result.errors().next().is_none());
}
}