From 355b45b467457f46011346791fb610140dfa4d8d Mon Sep 17 00:00:00 2001 From: Vlad Nedelcu Date: Sun, 6 Aug 2023 17:47:51 +0300 Subject: [PATCH] upgrade to new python loader engine --- Cargo.lock | 31 ++- Cargo.toml | 3 +- README.md | 7 +- src/joneslib/docstrings.rs | 122 ------------ src/joneslib/loader.rs | 190 ++++++++++++++++++ src/joneslib/markers.rs | 88 --------- src/joneslib/mod.rs | 182 ++--------------- src/joneslib/objects.rs | 90 ++++----- src/joneslib/parser.rs | 56 ++++++ src/joneslib/utils.rs | 394 ------------------------------------- src/main.rs | 11 +- 11 files changed, 333 insertions(+), 841 deletions(-) delete mode 100644 src/joneslib/docstrings.rs create mode 100644 src/joneslib/loader.rs delete mode 100644 src/joneslib/markers.rs create mode 100644 src/joneslib/parser.rs delete mode 100644 src/joneslib/utils.rs diff --git a/Cargo.lock b/Cargo.lock index db6bb83..12e432a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,11 +81,12 @@ dependencies = [ [[package]] name = "jones" -version = "0.1.4" +version = "0.2.0" dependencies = [ "ansi_term 0.12.1", "regex", "structopt", + "walkdir", ] [[package]] @@ -165,6 +166,15 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "strsim" version = "0.8.0" @@ -245,6 +255,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "winapi" version = "0.3.9" @@ -261,6 +281,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 8380498..c8e738e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jones" -version = "0.1.4" +version = "0.2.0" authors = ["Vlad Nedelcu "] license = "MIT" edition = "2018" @@ -12,6 +12,7 @@ categories = ["command-line-utilities"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +walkdir = "2.3.1" regex = "1.5.5" ansi_term = "0.12.1" structopt = "0.3.13" \ No newline at end of file diff --git a/README.md b/README.md index 51c6597..38a1f4d 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,9 @@ $ jones Tool ~/band_project Output: ```bash -# Class :: [Board] - -Tic Tac Toe board - +# Class [Board] +------- +*docstring: Tic Tac Toe board * inherit -> Tool # Methods diff --git a/src/joneslib/docstrings.rs b/src/joneslib/docstrings.rs deleted file mode 100644 index dba240f..0000000 --- a/src/joneslib/docstrings.rs +++ /dev/null @@ -1,122 +0,0 @@ -use super::{DOCSTRING, NEWLINE, INIT_DEF}; - - -pub fn extract_docstring(code_block: &Vec) -> Option { - let docstring_vec = match get_docstring(&code_block) { - Some(docstring) => docstring, - None => return None - }; - - return Some(docstring_vec.join(NEWLINE)) -} - -fn get_docstring(code_block: &Vec) -> Option> { - let mut start_docstring: bool = false; - let mut docstring_vec: Vec = Vec::new(); - - for line in code_block.iter() { - if !start_docstring && line.contains(DOCSTRING) { - start_docstring = true; - docstring_vec.push(format_line(line)); - match line.matches(DOCSTRING).count() { - 2 => break, - _ => continue - } - } else if line.contains(INIT_DEF) { - break - } - - if start_docstring { - docstring_vec.push(format_line(line)); - match line.contains(DOCSTRING) { - true => break, - false => continue - } - } - } - - match docstring_vec.len() { - 0 => None, - _ => Some(docstring_vec) - } -} - -fn format_line(line: &str) -> String { - return line.trim().replace(DOCSTRING, "") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_line(){ - let test_string = "\"\"\"Test docstring"; - let expected = "Test docstring"; - - assert_eq!(format_line(test_string), expected); - } - - #[test] - fn test_get_docstring(){ - let test_code_block: Vec = vec![ - "class God:".to_string(), - " \"\"\"DocString\"\"\"".to_string(), - "".to_string(), - " def __init__(self, name: int):".to_string(), - " self.name = name".to_string(), - "".to_string() - ]; - let expected = vec!["DocString".to_string()]; - - assert_eq!(get_docstring(&test_code_block), Some(expected)); - } - - #[test] - fn test_get_docstring_none(){ - let test_code_block: Vec = vec![ - "class God:".to_string(), - " pass".to_string(), - "".to_string() - ]; - assert_eq!(get_docstring(&test_code_block), None); - } - - #[test] - fn test_extract_docstring_some(){ - let test_code_block: Vec = vec![ - "class God:".to_string(), - " \"\"\"".to_string(), - " DocString".to_string(), - " Some more test".to_string(), - " \"\"\"".to_string(), - "".to_string(), - " def __init__(self, name: int):".to_string(), - " self.name = name".to_string(), - "".to_string() - ]; - let expected = vec![ - "".to_string(), - "DocString".to_string(), - "Some more test".to_string(), - "".to_string() - ]; - - assert_eq!(extract_docstring(&test_code_block), Some(expected.join(NEWLINE))); - - } - - #[test] - fn test_extract_docstring_none(){ - let test_code_block: Vec = vec![ - "class God:".to_string(), - "".to_string(), - " def __init__(self, name: int):".to_string(), - " \"\"\"Docstring test\"\"\"".to_string(), - " self.name = name".to_string(), - "".to_string() - ]; - - assert_eq!(extract_docstring(&test_code_block), None); - } -} \ No newline at end of file diff --git a/src/joneslib/loader.rs b/src/joneslib/loader.rs new file mode 100644 index 0000000..e7dfa3d --- /dev/null +++ b/src/joneslib/loader.rs @@ -0,0 +1,190 @@ +/* +JonesCLI + +Author: Vlad Nedelcu +Date: Jul 2021 +License: MIT + +Copyright 2021 Vlad Nedelcu +*/ +use std::{path::PathBuf, process::Command}; + +use regex::{Regex, RegexBuilder}; + +use super::{objects::PythonClass, parser::parse_class}; + +static CLASS_NAME_PATTERN: &str = r" (\w+)"; +static FILE_NAME_PATTERN: &str = r" (.+)"; +static METHODS_PATTERN: &str = r" (\w+)"; +static ATTRIBUTES_PATTERN: &str = r"\s\[(.*?)\]"; +static DOCSTRING_PATTERN: &str = r" <#(.*)#>"; +static INHERITANCE_PATTERN: &str = r"\s\[(.*?)\]"; +static OUTPUT_PATTERN: &str = r" (\w+)"; + +/// Loads all objects from a Python project, given through the python project path. +pub fn load_python_project(project_path: &PathBuf) -> Option> { + let class_name_pattern = Regex::new(CLASS_NAME_PATTERN).unwrap(); + let file_name_pattern = Regex::new(FILE_NAME_PATTERN).unwrap(); + + let script_output = match run_python_script(&project_path) { + Some(output) => String::from_utf8(output).unwrap(), + None => return None, + }; + + let file_pattern_captures = file_name_pattern.captures_iter(&script_output); + let found_classes = class_name_pattern + .captures_iter(&script_output) + .zip(file_pattern_captures) + .map(|(class_name, file_name)| (class_name[1].to_string(), file_name[1].to_string())) + .collect::>(); + + Some(found_classes) +} + +pub fn load_python_object(file_path: &PathBuf, class_name: &String) -> Option { + let script_output = match run_python_class_script(file_path, class_name) { + Some(output) => String::from_utf8(output).unwrap(), + None => return None, + }; + + let methods_pattern = Regex::new(METHODS_PATTERN).unwrap(); + let attributes_pattern = Regex::new(ATTRIBUTES_PATTERN).unwrap(); + let class_pattern = Regex::new(CLASS_NAME_PATTERN).unwrap(); + let docstring_pattern = RegexBuilder::new(DOCSTRING_PATTERN).dot_matches_new_line(true).build().unwrap(); + let inheritance_pattern = Regex::new(INHERITANCE_PATTERN).unwrap(); + let output_pattern = Regex::new(OUTPUT_PATTERN).unwrap(); + + let found_parameters = attributes_pattern.captures_iter(&script_output); + let found_outputs = output_pattern.captures_iter(&script_output); + let found_methods = methods_pattern + .captures_iter(&script_output) + .zip(found_parameters) + .zip(found_outputs) + .map(|((method, params), output)| { + ( + method[1].to_string(), + params[1].to_string(), + output[1].to_string(), + ) + }) + .collect::>(); + let found_class = class_pattern + .captures_iter(&script_output) + .map(|class_name| class_name[1].to_string()) + .collect::>(); + let found_docstring = docstring_pattern + .captures_iter(&script_output) + .map(|docstring| docstring[1].to_string()) + .collect::>(); + let found_inheritance = inheritance_pattern + .captures_iter(&script_output) + .map(|inheritance| inheritance[1].to_string()) + .collect::>(); + + let class_name = found_class[0].clone(); + let docstring = found_docstring[0].clone(); + let inheritance = found_inheritance[0] + .clone() + .split(", ") + .map(|s| s.to_string()) + .collect::>(); + + Some(parse_class( + class_name, + found_methods, + docstring, + inheritance, + )) +} + +#[inline] +fn run_python_script(project_path: &PathBuf) -> Option> { + let python_script = format!( + r#"import os +import ast + +for root, dirs, files in os.walk({:?}): + for name in files: + if name.endswith(".py"): + with open(os.path.join(root, name), 'r') as file: + tree = ast.parse(file.read()) + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + class_name = node.name + print(" %s, %s" % (class_name, os.path.join(root, name))) +"#, + project_path.as_os_str() + ); + + let output = Command::new("python").arg("-c").arg(python_script).output(); + + if let Ok(output) = output { + Some(output.stdout) + } else { + None + } +} + +#[inline] +fn run_python_class_script(file_path: &PathBuf, class_name: &String) -> Option> { + let python_script = format!( + r#"import ast + +with open({:?}, "r") as file: + tree = ast.parse(file.read()) + +def get_method(node_method): + method_name = node_method.name + method_args = [] + for arg in node_method.args.args: + arg_name = arg.arg + if isinstance(arg.annotation, ast.Name): + arg_type = arg.annotation.id + elif isinstance(arg.annotation, ast.Subscript): + arg_type = arg.annotation.value.id + elif isinstance(arg.annotation, ast.Attribute): + arg_type = arg.annotation.attr + else: + arg_type = None + + method_value = arg_name + "||" + str(arg_type) + method_args.append(method_value) + + return method_name, method_args + +def get_output(node_method): + if isinstance(node_method.returns, ast.Name): + return node_method.returns.id + elif isinstance(node_method.returns, ast.Subscript): + return node_method.returns.value.id + elif isinstance(node_method.returns, ast.Attribute): + return node_method.returns.attr + else: + return None + +for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == "{}": + class_name = node.name + for m in node.body: + if isinstance(m, ast.FunctionDef) or isinstance(m, ast.AsyncFunctionDef): + method_name, method_args = get_method(m) + print(" %s, [%s], %s" % (method_name, ", ".join(method_args), get_output(m))) + print(" %s" % (class_name)) + print(" <#%s#>" % (ast.get_docstring(node))) + print(" [%s]" % (', '.join([b.attr if isinstance(b, ast.Attribute) else b.id for b in node.bases] ))) + break +"#, + file_path.as_os_str(), + class_name + ); + + let output = Command::new("python").arg("-c").arg(python_script).output(); + + if let Ok(output) = output { + println!("{}", String::from_utf8(output.stderr.clone()).unwrap()); + Some(output.stdout) + } else { + None + } +} diff --git a/src/joneslib/markers.rs b/src/joneslib/markers.rs deleted file mode 100644 index 3a7de7a..0000000 --- a/src/joneslib/markers.rs +++ /dev/null @@ -1,88 +0,0 @@ -/* -JonesCLI - -Author: Vlad Nedelcu -Date: Jul 2021 -License: MIT - -Copyright 2021 Vlad Nedelcu -*/ - -use regex::Regex; - -const OPEN_PARANTHESES: char = '['; -const CLOSE_PARANTHESES: char = ']'; -const COMMA: char = ','; - - -/// Fetch the arguments from a class method and mark the commas -/// that will be used for splitting further -/// -/// # Arguments -/// -/// * `header` - The method header -/// -pub fn get_header_arguments(header: &String) -> Option { - let reg = Regex::new(r".*?\(|\).*").unwrap(); - let segments: Vec<&str> = reg.split(header) - .filter(|&entry| !entry.is_empty()) - .collect(); - - match segments.len() { - 0 => None, - _ => Some(mark_commas_for_split(segments[0])) - } -} - -/// Mark the commas that are not dividing the arguments in a method -/// class -/// -/// # Arguments -/// -/// * `args` - Arguments segment from a method header -fn mark_commas_for_split(args: &str) -> String { - let mut inside_brackets: bool = false; - let mut marked_args = args.to_string(); - - for (pos, ch) in args.chars().enumerate() { - match ch { - OPEN_PARANTHESES => inside_brackets = true, - CLOSE_PARANTHESES => inside_brackets = false, - COMMA => { - if inside_brackets { - marked_args.remove(pos+1); - } - }, - _ => continue - } - } - - marked_args -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mark_commas_for_split() { - let args = "param1: str, param2: Dict[str, int]"; - let expected = String::from("param1: str, param2: Dict[str,int]"); - - assert_eq!(mark_commas_for_split(args), expected); - } - - #[test] - fn test_get_header_arguments(){ - let header = String::from("def test_method(param1: str, param2: Dict[str, int]) -> str"); - let expected = String::from("param1: str, param2: Dict[str,int]"); - - assert_eq!(get_header_arguments(&header), Some(expected)); - } - - #[test] - fn test_get_header_no_arguments(){ - let header = String::from("def test_method() -> str"); - assert_eq!(get_header_arguments(&header), None); - } -} \ No newline at end of file diff --git a/src/joneslib/mod.rs b/src/joneslib/mod.rs index 3dc2714..824f32c 100644 --- a/src/joneslib/mod.rs +++ b/src/joneslib/mod.rs @@ -8,11 +8,10 @@ License: MIT Copyright 2021 Vlad Nedelcu */ -pub mod utils; pub mod objects; pub mod display; -pub mod markers; -pub mod docstrings; +pub mod loader; +pub mod parser; use std::fs; use std::path::PathBuf; @@ -21,59 +20,10 @@ const CLASS_TEMPLATE_INHERITANCE: &str = "class {template}("; const CLASS_TEMPLATE: &str = "class {template}:"; const TEMPLATE_KEYWORD: &str = "{template}"; const PYTHON_EXTENSION: &str = "py"; -const DOCSTRING: &str = "\"\"\""; -const NEWLINE: &str = "\n"; -const INIT_DEF: &str = "def __init__"; type ClassMatch = (String, String); - -/// Extracts the searched python class from the code -/// -/// # Arguments -/// -/// * `code_lines`: The full split into lines code file -/// -/// * `class_name`: The searched class name -/// -fn extract_python_class(code_lines: Vec<&str>, class_name: &str) -> objects::PythonClass { - let class_name_inheritance = CLASS_TEMPLATE_INHERITANCE.clone() - .replace(TEMPLATE_KEYWORD, class_name); - let class_name_simple = CLASS_TEMPLATE.clone() - .replace(TEMPLATE_KEYWORD, class_name); - - let mut start_cutting: bool = false; - let mut class_code_block: Vec = Vec::new(); - let mut class_header: String = String::from(""); - - for (counter, line) in code_lines.iter().enumerate() { - if line.contains(&class_name_inheritance) || line.contains(&class_name_simple) { - start_cutting = true; - class_header.push_str(line) - } - if start_cutting { - class_code_block.push(line.to_string()); - - if line.len() <= 1 && code_lines[counter-1].len() <= 1 { - break - } - } - } - - let class_inheritance = match utils::extract_class_inheritance(&class_header) { - Some(inheritance_vec) => inheritance_vec, - None => Vec::new() - }; - - let docstring = match docstrings::extract_docstring(&class_code_block) { - Some(inheritance_vec) => inheritance_vec, - None => String::from("None") - }; - - objects::PythonClass::new(class_code_block, class_name.to_string(), class_inheritance, docstring) -} - /// Check if a file contains the searched class by reading the file. /// /// # Arguments @@ -131,77 +81,36 @@ pub fn project_traversal(dir_path: &PathBuf, class_name: &String) -> Option continue } if check_file_contains_class(class_name, &file_path_name){ - let file_content = match fs::read_to_string(file_path) { - Ok(content) => content, - Err(_) => { - println!("Now skipping"); - continue - } - }; - let lines: Vec<&str> = file_content.split("\n").collect(); - - return Some(extract_python_class(lines, class_name)) + return loader::load_python_object(&file_path, &class_name) } } } return None } -/// Project traversal recursive and searches for a keyword based on itself or on context (Phase 2) -pub fn smart_search(dir_path: &PathBuf, class_name: &String) -> Option>{ - let mut found_matched_classes: Vec = Vec::new(); +/// Loads the project classes and filters them by the class name. Returns a vector +/// of tuples containing the class name and the file path. +pub fn search(dir_path: &PathBuf, class_name: &String) -> Option>{ - let current_dir = match fs::read_dir(dir_path) { - Ok(dir) => dir, - Err(err) => { - println!("Error occured while reading dir: {}", err); + let project_classes = match loader::load_python_project(dir_path) { + Some(classes) => classes, + None => { + println!("Error occurred while loading project classes"); return None } }; - for file in current_dir { - let file_path = file.unwrap().path(); - let file_path_name = file_path.to_str().unwrap(); - if file_path.is_dir() { - match smart_search(&file_path, class_name) { - Some(matches) => found_matched_classes.extend(matches), - None => continue - }; - } else { - match file_path.extension() { - Some(extension) => { - if extension != PYTHON_EXTENSION { - continue - } - }, - None => continue - }; - let file_content = match fs::read_to_string(&file_path) { - Ok(content) => content, - Err(_) => continue - }; - let lines: Vec<&str> = file_content.split("\n").collect(); - match utils::grep_class(lines, &class_name, file_path_name) { - Some(matches) => found_matched_classes.extend(matches), - None => continue - } - } - } + let filtered_classes = project_classes.into_iter() + .filter(|class| class.0.contains(class_name)) + .collect::>(); - if found_matched_classes.len() > 0 { - Some(found_matched_classes) - } else { - None - } + Some(filtered_classes) } #[cfg(test)] mod tests { - use super::objects::PythonClass; - use super::extract_python_class; use super::project_traversal; - use super::check_file_contains_class; use std::fs; use std::path::PathBuf; @@ -225,69 +134,6 @@ mod tests { print(f'My name is {self.age}') "; - #[test] - fn test_extract_python_class(){ - - let test_codebase = vec![ - "class God:".to_string(), - " \"\"\"DocString\"\"\"".to_string(), - "".to_string(), - " def __init__(self, name: int):".to_string(), - " self.name = name".to_string(), - "".to_string(), - " def hi(self):".to_string(), - " print(f'My name is {self.name}')".to_string(), - "".to_string() - ]; - let lines: Vec<&str> = PYTHON_CODE.split("\n").collect(); - - let expected_class = PythonClass::new(test_codebase, String::from("God"), Vec::new(), String::from("DocString")); - - assert_eq!(extract_python_class(lines, "God"), expected_class); - - } - - #[test] - fn test_check_file_contains_class() { - let path = "./test.py"; - fs::write(path, PYTHON_CODE).unwrap(); - - assert_eq!(true, check_file_contains_class("God", path)); - fs::remove_file(path).unwrap(); - } - - #[test] - fn test_check_file_contains_class_err() { - assert_eq!(check_file_contains_class("God", "./iogh.py"), false); - } - - #[test] - fn test_project_traversal() { - let path = "./testing/test.py"; - let mut pathbuf = PathBuf::new(); - pathbuf.push("./testing"); - fs::create_dir("./testing").expect("Could not write dire"); - fs::write(path, PYTHON_CODE).unwrap(); - - - let test_codebase = vec![ - "class God:".to_string(), - " \"\"\"DocString\"\"\"".to_string(), - "".to_string(), - " def __init__(self, name: int):".to_string(), - " self.name = name".to_string(), - "".to_string(), - " def hi(self):".to_string(), - " print(f'My name is {self.name}')".to_string(), - "".to_string() - ]; - - let expected_class = PythonClass::new(test_codebase, String::from("God"), Vec::new(), String::from("DocString")); - assert_eq!(expected_class, project_traversal(&pathbuf, &"God".to_string()).unwrap()); - - fs::remove_dir_all("./testing").expect("Could not delete dir"); - } - #[test] fn test_process_only_py_files() { diff --git a/src/joneslib/objects.rs b/src/joneslib/objects.rs index 962f640..f1ec2e2 100644 --- a/src/joneslib/objects.rs +++ b/src/joneslib/objects.rs @@ -8,95 +8,71 @@ License: MIT Copyright 2021 Vlad Nedelcu */ -use super::utils; -use std::fmt; use ansi_term::Colour; +use std::fmt; -#[derive(Debug)] -#[derive(PartialEq)] -pub struct Parameter{ +const SELF_PARAMETER: [&str; 2] = ["self", "cls"]; + +#[derive(Debug, PartialEq)] +pub struct Parameter { pub name: String, - pub static_type: String + pub static_type: String, } impl Parameter { - pub fn new(name: String, static_type: String) -> Self{ - Parameter { - name, - static_type - } - } + pub fn new(name: String, static_type: String) -> Self { + let annotation = if SELF_PARAMETER.contains(&&*name) { + "Self".to_string() + } else { + static_type.clone() + }; + Parameter { name, static_type: annotation } + } } -impl fmt::Display for Parameter{ +impl fmt::Display for Parameter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, " * {}: {}", + write!( + f, + " * {}: {}", Colour::Purple.paint(&self.name), Colour::Green.paint(&self.static_type) ) } } -#[derive(Debug)] -#[derive(PartialEq)] -pub struct Method{ +#[derive(Debug, PartialEq)] +pub struct Method { pub name: String, pub parameters: Vec, - pub output: String + pub output: String, } -impl Method { - pub fn new(method_header: &String) -> Self { - let method_name = match utils::extract_method_name(method_header) { - Ok(name) => name, - Err(err) => { - println!("Error while extracting method name: {}", err); - String::from("ENL") - } - }; - let method_output = match utils::extract_method_output(method_header) { - Ok(output) => output, - Err(_) => String::from("None") - }; - Method { - name: method_name, - output: method_output, - parameters: utils::extract_parameters(method_header) - } - } -} -impl fmt::Display for Method{ +impl fmt::Display for Method { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, ":: [{}] -> {}", + write!( + f, + ":: [{}] -> {}", Colour::Yellow.paint(&self.name), Colour::Cyan.paint(&self.output) ) } } -#[derive(Debug)] -#[derive(PartialEq)] -pub struct PythonClass{ +#[derive(Debug, PartialEq)] +pub struct PythonClass { pub name: String, pub methods: Vec, pub inheritance: Vec, - pub docstring: String + pub docstring: String, } -impl PythonClass { - pub fn new(class_code: Vec, name: String, inheritance: Vec, docstring: String) -> Self { - PythonClass { - name: name, - methods: utils::extract_methods(class_code), - inheritance, - docstring - } - } -} -impl fmt::Display for PythonClass{ +impl fmt::Display for PythonClass { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let inheritance_display = self.inheritance.join(", "); - write!(f, "# Class :: [{}]\n{}\n* inherit -> {}\n\n# Methods\n-------", + write!( + f, + "# Name [{}]\n--------\n* docstring: {}\n* inherits -> {}\n\n# Methods\n-------", Colour::Cyan.paint(&self.name), Colour::Yellow.paint(&self.docstring), Colour::Green.paint(inheritance_display) ) } -} \ No newline at end of file +} diff --git a/src/joneslib/parser.rs b/src/joneslib/parser.rs new file mode 100644 index 0000000..17136a8 --- /dev/null +++ b/src/joneslib/parser.rs @@ -0,0 +1,56 @@ +/* +JonesCLI + +Author: Vlad Nedelcu +Date: Jul 2021 +License: MIT + +Copyright 2021 Vlad Nedelcu +*/ +use super::objects::{Parameter, Method, PythonClass}; + +static PARAMETER_SEPARATOR: &str = "||"; + +pub fn parse_method_parameter(parameters: Vec) -> Vec{ + let mut parsed_parameters = Vec::new(); + for parameter in parameters.iter() { + let values = parameter.split(PARAMETER_SEPARATOR).collect::>(); + let (param_name, param_annotation) = (values[0], values[1]); + parsed_parameters.push(Parameter::new(param_name.to_string(), param_annotation.to_string())); + } + + parsed_parameters +} + +pub fn parse_method(methods_data: Vec<(String, String, String)>) -> Vec { + let mut parsed_methods = Vec::new(); + for method in methods_data.iter() { + let (method_name, method_parameters, method_output) = (method.0.clone(), method.1.clone(), method.2.clone()); + let raw_parameters = method_parameters.split(",").map(|x| x.to_string()).collect::>(); + let parameters = parse_method_parameter(raw_parameters); + parsed_methods.push( + Method { + name: method_name.to_string(), + parameters, + output: method_output + } + ); + } + + parsed_methods +} + +pub fn parse_class( + name: String, + methods: Vec<(String, String, String)>, + docstring: String, + inheritance: Vec, +) -> PythonClass { + let methods = parse_method(methods); + PythonClass { + name, + methods, + docstring, + inheritance + } +} \ No newline at end of file diff --git a/src/joneslib/utils.rs b/src/joneslib/utils.rs deleted file mode 100644 index 83db054..0000000 --- a/src/joneslib/utils.rs +++ /dev/null @@ -1,394 +0,0 @@ -/* -JonesCLI - -Author: Vlad Nedelcu -Date: Jul 2021 -License: MIT - -Copyright 2021 Vlad Nedelcu -*/ - -use regex::Regex; -use super::{ - objects::{ - Parameter, - Method - }, - markers::get_header_arguments -}; - -static FUNCTION_KEYWORD: &str = " def "; -static DEFAULT_TYPE: &str = "None"; -static ENDEF_KEYWORD: char = ':'; -static CLASS_KEYWORD: &str = "class "; - -/// Simple regex split on a given code line -/// # Arguments -/// -/// * `r`: Regex string line -/// * `trim`: boolean if you want to trim the values -/// * `value`: value to split -fn regex_split<'a>(r: &'a str, trim: bool, value: &'a String) -> Vec<&'a str> { - let regex_separator = Regex::new(r).expect("Invalid regex function given"); - match trim { - true => regex_separator.split(value.trim()).collect(), - false => regex_separator.split(value).collect() - } -} - -/// Extract a python method name from its method header -/// -/// # Arguments -/// -/// * `method_header`: Method header code line -/// -/// # Example -/// ```python -/// def method_name(self, arg1: int, arg2: str) -> None: -/// ``` -/// Extracted name here is `method_name` -pub fn extract_method_name(method_header: &String) -> Result { - let split_header = regex_split(r"\W", true, method_header); - if split_header[0].trim() != FUNCTION_KEYWORD.trim() { - return Err("This is not a method header") - } - Ok(split_header[1].to_string()) -} - -/// Extract method parameters with their static type -/// -/// # Arguments -/// -/// * `header`: - The header line from a Python method -pub fn extract_parameters(header: &String) -> Vec { - - // Split to get all the parameter - let parameter_segment: String = match get_header_arguments(header) { - Some(params) => params, - None => return Vec::new() - }; - let parameters_values = regex_split(r",\s", true, ¶meter_segment); - let mut parameters: Vec = Vec::new(); - - /* - Starting from the second value, iterate and extract - all the parameters from the method - */ - for param in parameters_values.iter(){ - - let param_string = param.to_string(); - let param_values: Vec<&str> = regex_split(r":\s", false, ¶m_string); - match param_values.len() { - 1 => { - parameters.push( - Parameter::new( - param_values[0].trim().to_string(), - DEFAULT_TYPE.to_string(), - ) - ) - }, - 2 => { - parameters.push( - Parameter::new( - param_values[0].trim().to_string(), - param_values[1].trim().to_string(), - ) - ) - }, - _ => println!("Found method in code with no parameters. {:?}", param_values) - } - - } - parameters -} - -/// Extract methods found in a Python class -/// -/// # Arguments -/// -/// * `class_code` - The code for the Python class extracted from the -/// .py file -pub fn extract_methods(class_code: Vec) -> Vec { - - // Initialize temp method and start for retrieving method headers - // that span on multiple lines - let mut methods: Vec = Vec::new(); - let mut temp_method = String::new(); - let mut start = false; - - for line in class_code.iter() { - if !start && line.contains(FUNCTION_KEYWORD) { - start = true; - } - if start { - temp_method.push_str(format!(" {}", line.trim()).as_str()); - let last_char = match temp_method.chars() - .nth(temp_method.len() - 1) { - Some(chr) => chr, - None => continue - }; - - if last_char == ENDEF_KEYWORD { - methods.push(Method::new(&temp_method)); - temp_method = String::new(); - start = false; - } - } - } - methods -} - -/// Extract method output after the poiting arrow -/// -/// # Arguments -/// -/// * `header` - Python method header -/// -/// # Output -/// -/// * `Err` - if the header had no type and at split nothing happened -/// * `Ok` - returns header type -pub fn extract_method_output(header: &String) -> Result { - let header_split = regex_split(r"( -> )", true, header); - match header_split.len() { - 1 => Err("Output type not found"), - _ => Ok(header_split[1].trim().replace(":", "")) - } -} - -/// Search & find all Python classes that contain the keyword or are relevant to the context -/// -/// # Arguments -/// -/// * `lines` - The python file code lines previously read -/// * `keyword` - The class name given for the search -/// -/// # Output -/// -/// * `Option>` - containing all the found relevant classes -pub fn grep_class<'a>(lines: Vec<&str>, keyword: &String, file_name: &str) -> Option> { - let mut found_match_classes: Vec<(String, String)> = Vec::new(); - for line in lines.iter() { - if line.trim().starts_with(CLASS_KEYWORD) && line.contains(keyword) { - found_match_classes.push( - (line.to_string(), file_name.to_string()) - ); - } - } - match found_match_classes.len() { - 0 => None, - _ => Some(found_match_classes) - } -} - - -/// Extract class inheritance objects -/// -/// > Note: Does not include extracting filters or constraints -/// -/// # Arguments -/// -/// * `line` - Is the class header previously extracted -/// -/// # Output -/// -/// A vector containing all the objects the class inherits -pub fn extract_class_inheritance(line: &String) -> Option> { - - // Split the header for now to get objects that are inherited by the class - let class_header_split: Vec<&str> = regex_split(r"(\(|\):|,)", true, line); - if class_header_split.len() == 1 { - return None - } - - // Iterate through the split results but ommit the first entry - let mut class_inheritance: Vec = Vec::new(); - for inheritance in class_header_split[1..class_header_split.len()-1].iter(){ - let inherit_object = inheritance.trim().to_string(); - class_inheritance.push(inherit_object); - } - - Some(class_inheritance) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_regex_split_positive(){ - let test_string = String::from("test1:test2"); - let expected = vec!["test1", "test2"]; - - let values: Vec<&str> = regex_split(r":", false, &test_string); - - assert_eq!(values, expected); - } - - #[test] - fn test_regex_split_trim(){ - let test_string = String::from(" test1:test2 "); - let expected = vec!["test1", "test2"]; - - let values: Vec<&str> = regex_split(r":", true, &test_string); - - assert_eq!(values, expected); - } - - #[test] - fn test_extract_method_name(){ - let test_string = String::from("def this_name(self, param2: int) -> None:"); - let expected = String::from("this_name"); - - assert_eq!(extract_method_name(&test_string).unwrap(), expected); - } - - #[test] - fn test_extract_method_name_negative(){ - let test_string = String::from("import definition as positive"); - assert!(extract_method_name(&test_string).is_err()); - } - - #[test] - fn test_extract_parameters_positive(){ - let test_string = String::from("def this_name(param1: str, param2: int) -> None:"); - let expected_parameters = vec![ - Parameter::new(String::from("param1"), String::from("str")), - Parameter::new(String::from("param2"), String::from("int")), - ]; - - assert_eq!(extract_parameters(&test_string), expected_parameters); - } - - #[test] - fn test_extract_parameters_one_parameter(){ - let test_string = String::from("def this_name(self) -> None:"); - let expected_parameters = vec![ - Parameter::new(String::from("self"), String::from("None")), - ]; - - assert_eq!(extract_parameters(&test_string), expected_parameters); - } - - #[test] - fn test_extract_parameters_no_parameter(){ - let test_string = String::from("def this_name() -> None:"); - let expected_parameters = Vec::new(); - - assert_eq!(extract_parameters(&test_string), expected_parameters); - } - - #[test] - fn test_extract_methods_positive(){ - let test_codebase = vec![ - "class Test:".to_string(), - "".to_string(), - " def __init__(self, name):".to_string(), - " self.name = name".to_string(), - "".to_string(), - " def say_hi(self):".to_string(), - " self.name = name".to_string(), - "".to_string() - ]; - - let expected_methods = vec![ - Method::new(&" def __init__(self, name):".to_string()), - Method::new(&" def say_hi(self):".to_string()), - ]; - - assert_eq!(extract_methods(test_codebase), expected_methods); - } - - #[test] - fn test_extract_methods_multiple_lines(){ - let test_codebase = vec![ - "class Test:".to_string(), - "".to_string(), - " def __init__(self, name: int,".to_string(), - " param1: str,".to_string(), - " param2: int) -> str:".to_string(), - " self.name = name".to_string(), - "".to_string(), - " def say_hi(self):".to_string(), - " self.name = name".to_string(), - "".to_string() - ]; - - let expected_methods = vec![ - Method::new(&" def __init__(self, name: int, param1: str, param2: int) -> str:".to_string()), - Method::new(&" def say_hi(self):".to_string()), - ]; - - assert_eq!(extract_methods(test_codebase), expected_methods); - } - - #[test] - fn test_extract_method_output() { - let test_string = String::from("def this_name(self, param2: int) -> List[int]:"); - let expected = String::from("List[int]"); - - assert_eq!(extract_method_output(&test_string).unwrap(), expected); - } - - #[test] - fn test_grep_class_some() { - let test_codebase = vec![ - "class God:", - "", - " def __init__(self, name):", - " self.name = name", - "", - "class GodMode:", - "", - " def __init__(self, name):", - " self.name = name", - "", - ]; - - let keyword = String::from("God"); - let filename = "./testing"; - let expected = vec![ - (String::from("class God:"), filename.to_string()), - (String::from("class GodMode:"), filename.to_string()), - ]; - - assert_eq!(grep_class(test_codebase, &keyword, filename).unwrap(), expected); - } - - #[test] - fn test_grep_class_none() { - let test_codebase = vec![ - "class God:", - "", - " def __init__(self, name):", - " self.name = name", - "", - "class GodMode:", - "", - " def __init__(self, name):", - " self.name = name", - "", - ]; - - let keyword = String::from("Zeus"); - let filename = "./testing"; - - - assert_eq!(grep_class(test_codebase, &keyword, filename), None); - } - - #[test] - fn test_extract_class_inheritance() { - let test_header = String::from("class Human(Being, Earthling):"); - let expected = vec![String::from("Being"), String::from("Earthling")]; - - assert_eq!(extract_class_inheritance(&test_header), Some(expected)); - } - - #[test] - fn test_extract_no_inheritance(){ - let test_header = String::from("class Human:"); - - assert_eq!(extract_class_inheritance(&test_header), None); - } -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 775b23f..6c80a59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,26 +8,25 @@ License: MIT Copyright 2021 Vlad Nedelcu */ -mod joneslib; mod commands; +mod joneslib; -use structopt::StructOpt; use joneslib::display; +use structopt::StructOpt; fn main() { let comms = commands::CLI::from_args(); if comms.grep { // Search for a keyword in class name - match joneslib::smart_search(&comms.dir_path, &comms.class_name) { + match joneslib::search(&comms.dir_path, &comms.class_name) { Some(matches) => display::class_matches(matches), - None => display::not_found_message() + None => display::not_found_message(), } } else { // Generate python class match joneslib::project_traversal(&comms.dir_path, &comms.class_name) { Some(class) => joneslib::display::output_class(&class), - None => display::not_found_message() + None => display::not_found_message(), } } } -