use lib_ruby_parser::nodes::Def;
use lib_ruby_parser::Node;
use crate::ast_helpers::{
body_expressions, def_block_arg_name, def_first_arg_name, def_regular_arg_count,
};
use crate::offense::{Offense, OffenseKind};
pub fn scan(def: &Def) -> Vec<Offense> {
let mut offenses = Vec::new();
check_proc_call_vs_yield(def, &mut offenses);
check_getter_vs_attr_reader(def, &mut offenses);
check_setter_vs_attr_writer(def, &mut offenses);
offenses
}
fn check_proc_call_vs_yield(def: &Def, offenses: &mut Vec<Offense>) {
let block_name = match def_block_arg_name(def) {
Some(name) => name,
None => return,
};
if body_contains_block_call(&def.body, &block_name) {
offenses.push(Offense::new(
OffenseKind::ProcCallVsYield,
def.keyword_l.begin,
));
}
}
fn body_contains_block_call(body: &Option<Box<Node>>, block_name: &str) -> bool {
match body.as_deref() {
Some(node) => node_contains_block_call(node, block_name),
None => false,
}
}
fn node_contains_block_call(node: &Node, block_name: &str) -> bool {
if let Node::Send(s) = node {
if s.method_name == "call" {
if let Some(Node::Lvar(lv)) = s.recv.as_deref() {
if lv.name == block_name {
return true;
}
}
}
}
let mut found = false;
crate::ast_helpers::for_each_child(node, |child| {
if !found && node_contains_block_call(child, block_name) {
found = true;
}
});
found
}
fn check_getter_vs_attr_reader(def: &Def, offenses: &mut Vec<Offense>) {
if def.name.ends_with('=') {
return;
}
if def_regular_arg_count(def) != 0 {
return;
}
let exprs = body_expressions(&def.body);
if exprs.len() != 1 {
return;
}
if let Node::Ivar(iv) = exprs[0] {
let expected_ivar = format!("@{}", def.name);
if iv.name == expected_ivar {
offenses.push(Offense::new(
OffenseKind::GetterVsAttrReader,
def.keyword_l.begin,
));
}
}
}
fn check_setter_vs_attr_writer(def: &Def, offenses: &mut Vec<Offense>) {
let base_name = match def.name.strip_suffix('=') {
Some(n) => n,
None => return,
};
if def_regular_arg_count(def) != 1 {
return;
}
let arg_name = match def_first_arg_name(def) {
Some(name) => name,
None => return,
};
let exprs = body_expressions(&def.body);
if exprs.len() != 1 {
return;
}
if let Node::Ivasgn(ia) = exprs[0] {
let expected_ivar = format!("@{}", base_name);
if ia.name != expected_ivar {
return;
}
if let Some(Node::Lvar(lv)) = ia.value.as_deref() {
if lv.name == arg_name {
offenses.push(Offense::new(
OffenseKind::SetterVsAttrWriter,
def.keyword_l.begin,
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast_helpers::node_children;
fn parse_and_scan(source: &[u8]) -> Vec<Offense> {
let result = lib_ruby_parser::Parser::new(source.to_vec(), Default::default()).do_parse();
let mut offenses = Vec::new();
if let Some(ast) = result.ast {
collect_def_offenses(&ast, &mut offenses);
}
offenses
}
fn collect_def_offenses(node: &Node, offenses: &mut Vec<Offense>) {
if let Node::Def(d) = node {
offenses.extend(scan(d));
}
for child in node_children(node) {
collect_def_offenses(child, offenses);
}
}
#[test]
fn getter_fires() {
let offenses = parse_and_scan(b"def name; @name; end");
assert!(offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn getter_with_assignment_does_not_fire() {
let offenses = parse_and_scan(b"def name; @name = 1; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn setter_fires() {
let offenses = parse_and_scan(b"def name=(value); @name = value; end");
assert!(offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
#[test]
fn proc_call_fires() {
let offenses = parse_and_scan(b"def foo(&block); block.call; end");
assert!(offenses
.iter()
.any(|o| o.kind == OffenseKind::ProcCallVsYield));
}
#[test]
fn no_block_arg_no_proc_call() {
let offenses = parse_and_scan(b"def foo; block.call; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::ProcCallVsYield));
}
#[test]
fn setter_wrong_ivar_name_no_fire() {
let offenses = parse_and_scan(b"def name=(v); @other = v; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
#[test]
fn setter_wrong_value_no_fire() {
let offenses = parse_and_scan(b"def name=(v); @name = 42; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
#[test]
fn setter_multiple_args_no_fire() {
let offenses = parse_and_scan(b"def name=(a, b); @name = a; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
#[test]
fn setter_no_body_no_fire() {
let offenses = parse_and_scan(b"def name=(v); end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
#[test]
fn getter_with_args_no_fire() {
let offenses = parse_and_scan(b"def name(x); @name; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn getter_multiple_body_stmts_no_fire() {
let offenses = parse_and_scan(b"def name; puts 'x'; @name; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn getter_wrong_ivar_no_fire() {
let offenses = parse_and_scan(b"def name; @other; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn getter_no_body_no_fire() {
let offenses = parse_and_scan(b"def name; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn proc_call_nested_in_body() {
let offenses = parse_and_scan(b"def foo(&block); if true; block.call; end; end");
assert!(offenses
.iter()
.any(|o| o.kind == OffenseKind::ProcCallVsYield));
}
#[test]
fn setter_name_method_is_not_getter() {
let offenses = parse_and_scan(b"def name=(v); @name = v; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::GetterVsAttrReader));
}
#[test]
fn setter_body_not_ivasgn_no_fire() {
let offenses = parse_and_scan(b"def name=(v); puts v; end");
assert!(!offenses
.iter()
.any(|o| o.kind == OffenseKind::SetterVsAttrWriter));
}
}