use std::{collections::HashMap, fmt};
use crate::UnwrapToError;
use git_function_history_proc_macro::enumstuff;
use lib_ruby_parser::{
nodes::{Class, Def},
source::DecodedInput,
Loc, Parser, ParserOptions,
};
use super::FunctionTrait;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RubyFunction {
pub name: String,
pub lines: (usize, usize),
pub class: Vec<RubyClass>,
pub args: RubyParams,
pub body: String,
}
impl RubyFunction {
pub const fn new(
name: String,
lines: (usize, usize),
class: Vec<RubyClass>,
args: RubyParams,
body: String,
) -> Self {
Self {
name,
lines,
class,
args,
body,
}
}
}
impl fmt::Display for RubyFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for class in &self.class {
write!(f, "{}", class.top)?;
}
write!(f, "{}", self.body)?;
for class in &self.class {
write!(f, "{}", class.bottom)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RubyClass {
pub name: String,
pub lines: (usize, usize),
pub superclass: Option<String>,
pub top: String,
pub bottom: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RubyParams {
args: Vec<String>,
kwargs: Vec<String>,
kwnilarg: bool,
forwarded_args: bool,
kwoptargs: Vec<(String, String)>,
kwrestarg: Option<String>,
restarg: Option<String>,
}
impl RubyParams {
pub const fn new() -> Self {
Self {
args: Vec::new(),
kwnilarg: false,
forwarded_args: false,
kwrestarg: None,
restarg: None,
kwargs: Vec::new(),
kwoptargs: Vec::new(),
}
}
fn contains(&self, name: &String) -> bool {
self.args.contains(name)
|| self.kwargs.contains(name)
|| self
.kwoptargs
.iter()
.any(|(arg, default)| arg == name || default == name)
|| Some(name.clone()) == self.kwrestarg
|| Some(name.clone()) == self.restarg
}
}
impl Default for RubyParams {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn find_function_in_file(
file_contents: &str,
name: &str,
) -> Result<Vec<RubyFunction>, String> {
let mut starts = file_contents
.match_indices('\n')
.map(|x| x.0)
.collect::<Vec<_>>();
starts.push(0);
starts.sort_unstable();
let map = starts
.iter()
.enumerate()
.collect::<HashMap<usize, &usize>>();
let parser = Parser::new(file_contents, ParserOptions::default());
let parsed = parser.do_parse();
for d in parsed.diagnostics {
if d.is_error() {
return Err(d.message.render());
}
}
let ast = parsed.ast.unwrap_to_error("Failed to parse file")?;
let fns = get_functions_from_node(&ast, &vec![], name);
let index = super::turn_into_index(file_contents)?;
let fns = fns
.iter()
.map(|(f, c)| {
let class = c
.iter()
.filter_map(|c| {
let start_line = super::get_from_index(&index, c.expression_l.begin)?;
let end_line = super::get_from_index(&index, c.expression_l.end)?;
let loc_end = c.end_l;
let top = Loc {
begin: **map.get(&(start_line - 1))?,
end: c.body.as_ref().map_or(
c.superclass
.as_ref()
.map_or(c.name.expression().end, |c| c.expression().end),
|b| b.expression().begin,
) - 1,
};
let mut top = top.source(&parsed.input)?;
top = top.trim_matches('\n').to_string();
top = super::make_lined(&top, start_line);
Some(RubyClass {
name: parser_class_name(c),
lines: (start_line, end_line),
superclass: parse_superclass(c),
top,
bottom: super::make_lined(
loc_end
.with_begin(**map.get(&(end_line - 1))?)
.source(&parsed.input)?
.trim_matches('\n'),
end_line,
),
})
})
.collect();
let start = f.expression_l.begin;
let start_line = super::get_from_index(&index, start)
.unwrap_to_error("Failed to get start lines")?;
let end_line = super::get_from_index(&index, f.expression_l.end)
.unwrap_to_error("Failed to get end line")?;
let starts = start_line + 1;
Ok::<RubyFunction, String>(RubyFunction {
name: f.name.clone(),
lines: (start_line, end_line),
class,
body: super::make_lined(
f.expression_l
.with_begin(**map.get(&(start_line - 1)).unwrap_or(&&0))
.source(&parsed.input)
.unwrap_to_error("Failed to get function body from source")?
.trim_matches('\n'),
starts - 1,
),
args: f
.args
.clone()
.map_or_else(RubyParams::new, |a| parse_args_from_node(&a, &parsed.input)),
})
})
.filter_map(Result::ok)
.collect::<Vec<RubyFunction>>();
if fns.is_empty() {
Err("No functions with this name was found in the this file")?;
}
Ok(fns)
}
fn get_functions_from_node(
node: &lib_ruby_parser::Node,
class: &Vec<Class>,
name: &str,
) -> Vec<(Def, Vec<Class>)> {
match node {
lib_ruby_parser::Node::Def(def) => {
if def.name == name {
vec![(def.clone(), class.clone())]
} else {
vec![]
}
}
lib_ruby_parser::Node::Class(new_class) => {
let mut functions = vec![];
let mut new_list = class.clone();
new_list.push(new_class.clone());
for child in &new_class.body {
functions.extend(get_functions_from_node(child, &new_list, name));
}
functions
}
lib_ruby_parser::Node::Begin(stat) => {
let mut functions = vec![];
for child in &stat.statements {
functions.extend(get_functions_from_node(child, class, name));
}
functions
}
_ => vec![],
}
}
fn parse_args_from_node(node: &lib_ruby_parser::Node, parsed_file: &DecodedInput) -> RubyParams {
let mut ret_args = RubyParams::new();
if let lib_ruby_parser::Node::Args(args) = node {
args.args.iter().for_each(|arg| match arg {
lib_ruby_parser::Node::Arg(arg) => ret_args.args.push(arg.name.clone()),
lib_ruby_parser::Node::Kwarg(arg) => ret_args.kwargs.push(arg.name.clone()),
lib_ruby_parser::Node::Kwoptarg(arg) => {
ret_args.kwoptargs.push((
arg.name.clone(),
arg.default
.expression()
.source(parsed_file)
.unwrap_or_else(|| "could not retrieve default value".to_string()),
));
}
lib_ruby_parser::Node::Restarg(arg) => {
if let Some(name) = &arg.name {
ret_args.restarg = Some(name.clone());
}
}
lib_ruby_parser::Node::Kwrestarg(arg) => {
if let Some(name) = &arg.name {
ret_args.kwrestarg = Some(name.clone());
}
}
lib_ruby_parser::Node::ForwardArg(_) => ret_args.forwarded_args = true,
lib_ruby_parser::Node::Kwnilarg(_) => ret_args.kwnilarg = true,
_ => {}
});
};
ret_args
}
fn parser_class_name(class: &Class) -> String {
match class.name.as_ref() {
lib_ruby_parser::Node::Const(constant) => constant.name.clone(),
_ => String::new(),
}
}
fn parse_superclass(class: &Class) -> Option<String> {
class
.superclass
.as_ref()
.and_then(|superclass| match superclass.as_ref() {
lib_ruby_parser::Node::Const(constant) => Some(constant.name.clone()),
_ => None,
})
}
impl FunctionTrait for RubyFunction {
crate::impl_function_trait!(RubyFunction);
fn get_total_lines(&self) -> (usize, usize) {
self.class
.iter()
.map(|class| class.lines)
.min_by(Ord::cmp)
.unwrap_or(self.lines)
}
fn get_tops(&self) -> Vec<(String, usize)> {
self.class
.iter()
.map(|class| (class.top.clone(), class.lines.0))
.collect()
}
fn get_bottoms(&self) -> Vec<(String, usize)> {
self.class
.iter()
.map(|class| (class.bottom.clone(), class.lines.1))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, enumstuff)]
pub enum RubyFilter {
FunctionInClass(String),
FunctionWithParameter(String),
FunctionWithSuperClass(String),
}
impl RubyFilter {
pub fn matches(&self, function: &RubyFunction) -> bool {
match self {
Self::FunctionInClass(name) => function.class.iter().any(|class| &class.name == name),
Self::FunctionWithParameter(name) => function.args.contains(name),
Self::FunctionWithSuperClass(name) => function.class.iter().any(|class| {
class
.superclass
.as_ref()
.map_or(false, |superclass| superclass == name)
}),
}
}
}