use std::path::Path;
use lib_ruby_parser::{ErrorLevel, Node, Parser};
use crate::ast_helpers::{byte_offset_to_line, for_each_child};
use crate::comment_directives::build_disabled_set;
use crate::config::Config;
use crate::offense::Offense;
use crate::scanner::{
for_loop_scanner, method_call_scanner, method_definition_scanner, rescue_scanner,
};
#[derive(Debug)]
pub struct AnalysisResult {
pub path: String,
pub offenses: Vec<Offense>,
}
#[derive(Debug)]
pub struct ParseError {
pub path: String,
pub message: String,
}
pub fn analyze_file(path: &Path, config: &Config) -> Result<AnalysisResult, ParseError> {
let source = std::fs::read(path).map_err(|e| ParseError {
path: path.display().to_string(),
message: e.to_string(),
})?;
let newline_positions: Vec<usize> = source
.iter()
.enumerate()
.filter(|&(_, &b)| b == b'\n')
.map(|(i, _)| i)
.collect();
let source_clone = source.clone();
let result = Parser::new(source, Default::default()).do_parse();
let has_errors = result
.diagnostics
.iter()
.any(|d| d.level == ErrorLevel::Error);
if has_errors {
if result.ast.is_none() {
return Err(ParseError {
path: path.display().to_string(),
message: result
.diagnostics
.iter()
.filter(|d| d.level == ErrorLevel::Error)
.map(|d| format!("{:?}", d.message))
.collect::<Vec<_>>()
.join(", "),
});
}
return Ok(AnalysisResult {
path: path.display().to_string(),
offenses: vec![],
});
}
let ast = match result.ast {
Some(ast) => ast,
None => {
return Ok(AnalysisResult {
path: path.display().to_string(),
offenses: vec![],
});
}
};
let disabled_set = build_disabled_set(&result.comments, &source_clone, &newline_positions);
let mut offenses = Vec::new();
walk_node(&ast, &mut offenses, &source_clone);
let offenses = offenses
.into_iter()
.filter(|o| config.is_enabled(o.kind))
.map(|o| {
let line = byte_offset_to_line(&newline_positions, o.line);
Offense {
kind: o.kind,
line,
fix: o.fix,
}
})
.filter(|o| !disabled_set.is_disabled(o.line, o.kind))
.collect();
Ok(AnalysisResult {
path: path.display().to_string(),
offenses,
})
}
fn walk_node(node: &Node, offenses: &mut Vec<Offense>, source: &[u8]) {
match node {
Node::For(f) => {
offenses.extend(for_loop_scanner::scan(f, source));
for_each_child(node, |child| walk_node(child, offenses, source));
}
Node::RescueBody(rb) => {
offenses.extend(rescue_scanner::scan(rb));
for_each_child(node, |child| walk_node(child, offenses, source));
}
Node::Def(d) => {
offenses.extend(method_definition_scanner::scan(d));
for_each_child(node, |child| walk_node(child, offenses, source));
}
Node::Send(s) => {
if let Some(Node::Block(recv_block)) = s.recv.as_deref() {
offenses.extend(method_call_scanner::scan_send_on_block(s, recv_block));
}
offenses.extend(method_call_scanner::scan_send(s));
for_each_child(node, |child| walk_node(child, offenses, source));
}
Node::Block(b) => {
offenses.extend(method_call_scanner::scan_block(b));
if let Node::Send(s) = b.call.as_ref() {
if let Some(recv) = &s.recv {
walk_node(recv, offenses, source);
}
for arg in &s.args {
walk_node(arg, offenses, source);
}
}
if let Some(args) = &b.args {
walk_node(args, offenses, source);
}
if let Some(body) = &b.body {
walk_node(body, offenses, source);
}
}
_ => {
for_each_child(node, |child| walk_node(child, offenses, source));
}
}
}
#[cfg(test)]
mod tests {
use crate::ast_helpers::byte_offset_to_line;
fn newline_positions(source: &[u8]) -> Vec<usize> {
source
.iter()
.enumerate()
.filter(|&(_, &b)| b == b'\n')
.map(|(i, _)| i)
.collect()
}
#[test]
fn byte_offset_to_line_works() {
let source = b"line1\nline2\nline3";
let positions = newline_positions(source);
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 analyze_nonexistent_file_returns_error() {
let config = crate::config::Config::default();
let result = super::analyze_file(std::path::Path::new("/nonexistent.rb"), &config);
assert!(result.is_err());
}
#[test]
fn analyze_file_with_parse_errors_no_ast_returns_error() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("fatal.rb");
std::fs::write(&file, "\x00\x01\x02").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config);
let _ = result;
}
#[test]
fn analyze_file_with_recovered_ast_returns_empty() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("recovered.rb");
std::fs::write(&file, "def foo; end; def def; end").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config);
match result {
Ok(analysis) => assert!(analysis.offenses.is_empty()),
Err(_) => {} }
}
#[test]
fn analyze_empty_file_returns_empty() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("empty.rb");
std::fs::write(&file, "").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}
#[test]
fn analyze_file_with_config_disabling_rule() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "for x in [1]; end").unwrap();
let config =
crate::config::Config::parse_yaml("speedups:\n for_loop_vs_each: false\n").unwrap();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}
#[test]
fn analyze_file_with_inline_disable() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(
&file,
"for x in [1]; end # rubyfast:disable for_loop_vs_each\n",
)
.unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(result.offenses.is_empty());
}
#[test]
fn walk_node_block_with_non_send_call() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "arr.map { |x| x.to_s }").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(!result.offenses.is_empty());
}
#[test]
fn walk_node_nested_for_inside_method() {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("test.rb");
std::fs::write(&file, "def foo\n for x in [1,2]; puts x; end\nend\n").unwrap();
let config = crate::config::Config::default();
let result = super::analyze_file(&file, &config).unwrap();
assert!(
result
.offenses
.iter()
.any(|o| o.kind == crate::offense::OffenseKind::ForLoopVsEach)
);
}
}