use lib_ruby_parser::nodes::For;
use crate::fix::Fix;
use crate::offense::{Offense, OffenseKind};
pub fn scan(node: &For, source: &[u8]) -> Vec<Offense> {
match build_fix(node, source) {
Some(fix) => vec![Offense::with_fix(
OffenseKind::ForLoopVsEach,
node.keyword_l.begin,
fix,
)],
None => vec![Offense::new(
OffenseKind::ForLoopVsEach,
node.keyword_l.begin,
)],
}
}
fn build_fix(node: &For, source: &[u8]) -> Option<Fix> {
let iterator = extract_trimmed(source, node.keyword_l.end, node.operator_l.begin)?;
let begin_char = source.get(node.begin_l.begin).copied().unwrap_or(0);
let has_explicit_begin = !node.begin_l.is_empty() && begin_char != b'\n';
let (iteratee, header_end) = if has_explicit_begin {
let text = extract_trimmed(source, node.operator_l.end, node.begin_l.begin)?;
(text, node.begin_l.end)
} else {
let search_start = node.operator_l.end;
let line_end = source[search_start..]
.iter()
.position(|&b| b == b'\n')
.map(|p| search_start + p)
.unwrap_or(source.len());
let text = extract_trimmed(source, search_start, line_end)?;
(text, line_end)
};
if iterator.is_empty() || iteratee.is_empty() {
return None;
}
let new_header = format!("{}.each do |{}|", iteratee, iterator);
Some(Fix::single(node.keyword_l.begin, header_end, new_header))
}
fn extract_trimmed(source: &[u8], start: usize, end: usize) -> Option<String> {
if start >= end || end > source.len() {
return None;
}
String::from_utf8(source[start..end].to_vec())
.ok()
.map(|s| s.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_loop_always_fires() {
let source = b"for x in [1,2,3]; end";
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let ast = result.ast.unwrap();
if let lib_ruby_parser::Node::For(f) = ast.as_ref() {
let offenses = scan(f, source);
assert_eq!(offenses.len(), 1);
assert_eq!(offenses[0].kind, OffenseKind::ForLoopVsEach);
assert!(offenses[0].fix.is_some());
} else {
panic!("Expected For node");
}
}
#[test]
fn fix_for_loop_with_do() {
let source = b"for x in arr do\n puts x\nend";
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let ast = result.ast.unwrap();
if let lib_ruby_parser::Node::For(f) = ast.as_ref() {
let fix = build_fix(f, source).unwrap();
let fixed = crate::fix::apply_fixes(source, &[fix]);
assert_eq!(
String::from_utf8(fixed).unwrap(),
"arr.each do |x|\n puts x\nend"
);
} else {
panic!("Expected For node");
}
}
#[test]
fn fix_for_loop_with_semicolon() {
let source = b"for x in [1,2,3]; puts x; end";
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let ast = result.ast.unwrap();
if let lib_ruby_parser::Node::For(f) = ast.as_ref() {
let fix = build_fix(f, source).unwrap();
let fixed = crate::fix::apply_fixes(source, &[fix]);
let fixed_str = String::from_utf8(fixed).unwrap();
assert!(fixed_str.starts_with("[1,2,3].each do |x|"));
} else {
panic!("Expected For node");
}
}
#[test]
fn fix_for_loop_newline_only() {
let source = b"for x in arr\n puts x\nend";
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let ast = result.ast.unwrap();
if let lib_ruby_parser::Node::For(f) = ast.as_ref() {
let fix = build_fix(f, source).unwrap();
let fixed = crate::fix::apply_fixes(source, &[fix]);
assert_eq!(
String::from_utf8(fixed).unwrap(),
"arr.each do |x|\n puts x\nend"
);
} else {
panic!("Expected For node");
}
}
#[test]
fn extract_trimmed_valid() {
let source = b" hello ";
let result = extract_trimmed(source, 0, 9);
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn extract_trimmed_start_ge_end() {
assert_eq!(extract_trimmed(b"hello", 5, 3), None);
assert_eq!(extract_trimmed(b"hello", 3, 3), None);
}
#[test]
fn extract_trimmed_end_gt_len() {
assert_eq!(extract_trimmed(b"hi", 0, 10), None);
}
#[test]
fn extract_trimmed_empty_after_trim() {
let result = extract_trimmed(b" ", 0, 3);
assert_eq!(result, Some("".to_string()));
}
#[test]
fn scan_always_returns_offense() {
let source = b"for x in arr; end";
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let ast = result.ast.unwrap();
if let lib_ruby_parser::Node::For(f) = ast.as_ref() {
let offenses = scan(f, source);
assert_eq!(offenses.len(), 1);
assert_eq!(offenses[0].kind, OffenseKind::ForLoopVsEach);
}
}
}