From bd183153c88a09cc59460c536c9c7f5d385171a3 Mon Sep 17 00:00:00 2001 From: Robin Quintero Date: Sat, 24 Feb 2024 14:18:42 -0500 Subject: [PATCH] feat(build): #1 add support for git repositories - Change version to `0.2.0` - Add support to analyze git repositories from GitHub and GitLab. - Calculate the cognitive complexity of the files in the repository. --- Cargo.lock | 58 +++++++++++++++++++++++++++++-- Cargo.toml | 3 +- complexipy/main.py | 42 +++++++++++++++++----- src/cognitive_complexity/mod.rs | 45 ++++++++++++++++++++---- src/cognitive_complexity/utils.rs | 14 ++++++++ 5 files changed, 144 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d097bb2..45bef17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "bstr" version = "1.9.0" @@ -113,7 +119,7 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "complexipy" -version = "1.0.0" +version = "0.2.0" dependencies = [ "env_logger", "ignore", @@ -121,6 +127,7 @@ dependencies = [ "pyo3", "rayon", "rustpython-parser", + "tempfile", ] [[package]] @@ -214,6 +221,22 @@ dependencies = [ "log", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "getopts" version = "0.2.21" @@ -308,6 +331,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.11" @@ -626,7 +655,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -673,6 +702,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustpython-ast" version = "0.3.1" @@ -819,6 +861,18 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + [[package]] name = "time" version = "0.3.20" diff --git a/Cargo.toml b/Cargo.toml index 641c77e..1863bfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "complexipy" -version = "1.0.0" +version = "0.2.0" edition = "2021" authors = ["Robin Quintero "] license = "MIT" @@ -22,3 +22,4 @@ log = "0.4.20" pyo3 = "0.19.0" rayon = "1.8.1" rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "9ce55aefdeb35e2f706ce0b02d5a2dfe6295fc57" } +tempfile = "3.10.0" diff --git a/complexipy/main.py b/complexipy/main.py index 679b250..ab0d8f4 100644 --- a/complexipy/main.py +++ b/complexipy/main.py @@ -1,6 +1,7 @@ from pathlib import Path from complexipy import rust import os +import re from rich.console import Console from rich.table import Table import time @@ -11,27 +12,39 @@ root_dir = Path(__file__).resolve().parent.parent app = typer.Typer(name="complexipy") console = Console() -version = "1.0.0" +version = "0.2.0" + @app.command() def main( path: str, - max_complexity: int = typer.Option(15, "--max-complexity", "-c", help="The maximum complexity allowed, set this value as 0 to set it as unlimited."), - output: bool = typer.Option(False, "--output", "-o", help="Output the results to a XML file."), + max_complexity: int = typer.Option( + 15, + "--max-complexity", + "-c", + help="The maximum complexity allowed, set this value as 0 to set it as unlimited.", + ), + output: bool = typer.Option( + False, "--output", "-o", help="Output the results to a XML file." + ), ): has_success = True is_dir = Path(path).is_dir() + _url_pattern = ( + r"^(https:\/\/|http:\/\/|www\.|git@)(github|gitlab)\.com(\/[\w.-]+){2,}$" + ) + is_url = bool(re.match(_url_pattern, path)) + invocation_path = os.getcwd() console.rule(f"complexipy {version} :octopus:") with console.status("Analyzing the complexity of the code...", spinner="dots"): start_time = time.time() - files = rust.main(path, is_dir, max_complexity) + files = rust.main(path, is_dir, is_url, max_complexity) execution_time = time.time() - start_time - console.print("Analysis completed! :tada:") + console.rule("Analysis completed! :tada:") # Output to XML if output: - invocation_path = os.getcwd() xml_file = ET.Element("complexity") for file in files: file_xml = ET.SubElement(xml_file, "file") @@ -49,21 +62,32 @@ def main( console.print(f"Results saved to {invocation_path}/complexipy.xml") # Summary - table = Table(title="Summary", show_header=True, header_style="bold magenta", show_lines=True) + table = Table( + title="Summary", show_header=True, header_style="bold magenta", show_lines=True + ) table.add_column("Path") table.add_column("File") table.add_column("Complexity") for file in files: if file.complexity > max_complexity and max_complexity != 0: - table.add_row(f"{file.path}", f"[green]{file.file_name}[/green]", f"[red]{file.complexity}[/red]") + table.add_row( + f"{file.path}", + f"[green]{file.file_name}[/green]", + f"[red]{file.complexity}[/red]", + ) has_success = False else: - table.add_row(f"{file.path}", f"[green]{file.file_name}[/green]", f"[blue]{file.complexity}[/blue]") + table.add_row( + f"{file.path}", + f"[green]{file.file_name}[/green]", + f"[blue]{file.complexity}[/blue]", + ) console.print(table) console.print(f"{len(files)} files analyzed in {execution_time:.4f} seconds") if not has_success: raise typer.Exit(code=1) + if __name__ == "__main__": app() diff --git a/src/cognitive_complexity/mod.rs b/src/cognitive_complexity/mod.rs index e3ec9d5..d1d40aa 100644 --- a/src/cognitive_complexity/mod.rs +++ b/src/cognitive_complexity/mod.rs @@ -8,20 +8,48 @@ use rustpython_parser::{ ast::{self, Stmt}, Parse, }; +use std::env; use std::path; -use utils::{count_bool_ops, has_recursive_calls, is_decorator}; +use std::process; +use tempfile::tempdir; +use utils::{count_bool_ops, get_repo_name, has_recursive_calls, is_decorator}; // Main function #[pyfunction] -pub fn main(path: &str, is_dir: bool, max_complexity: usize) -> PyResult> { +pub fn main( + path: &str, + is_dir: bool, + is_url: bool, + max_complexity: usize, +) -> PyResult> { let mut ans: Vec = Vec::new(); - if is_dir { + + if is_url { + let dir = tempdir()?; + let repo_name = get_repo_name(path); + + env::set_current_dir(&dir)?; + + let _output = process::Command::new("git") + .args(&["clone", path]) + .output() + .expect("failed to execute process"); + + let repo_path = dir.path().join(&repo_name).to_str().unwrap().to_string(); + + match evaluate_dir(&repo_path, max_complexity) { + Ok(files_complexity) => ans = files_complexity, + Err(e) => return Err(e), + } + + dir.close()?; + } else if is_dir { match evaluate_dir(path, max_complexity) { Ok(files_complexity) => ans = files_complexity, Err(e) => return Err(e), } } else { - match file_cognitive_complexity(path, max_complexity) { + match file_cognitive_complexity(path, path, max_complexity) { Ok(file_complexity) => ans.push(file_complexity), Err(e) => return Err(e), } @@ -35,6 +63,8 @@ pub fn main(path: &str, is_dir: bool, max_complexity: usize) -> PyResult PyResult> { let mut files_paths: Vec = Vec::new(); + let parent_dir = path::Path::new(path).parent().unwrap().to_str().unwrap(); + // Get all the python files in the directory for entry in Walk::new(path) { let entry = entry.unwrap(); @@ -48,7 +78,7 @@ pub fn evaluate_dir(path: &str, max_complexity: usize) -> PyResult, PyErr> = files_paths .par_iter() .map( - |file_path| match file_cognitive_complexity(file_path, max_complexity) { + |file_path| match file_cognitive_complexity(file_path, parent_dir, max_complexity) { Ok(file_complexity) => Ok(file_complexity), Err(e) => Err(e), }, @@ -65,6 +95,7 @@ pub fn evaluate_dir(path: &str, max_complexity: usize) -> PyResult PyResult { let code = std::fs::read_to_string(file_path)?; @@ -74,6 +105,8 @@ pub fn file_cognitive_complexity( let path = path::Path::new(file_path); let file_name = path.file_name().unwrap().to_str().unwrap(); + let relative_path = path.strip_prefix(base_path).unwrap().to_str().unwrap(); + for node in ast.iter() { complexity += statement_cognitive_complexity(node.clone(), 0)?; } @@ -81,7 +114,7 @@ pub fn file_cognitive_complexity( println!("{}", file_name); Ok(FileComplexity { - path: file_path.to_string(), + path: relative_path.to_string(), file_name: file_name.to_string(), complexity: complexity, }) diff --git a/src/cognitive_complexity/utils.rs b/src/cognitive_complexity/utils.rs index 7f528cf..02cc45e 100644 --- a/src/cognitive_complexity/utils.rs +++ b/src/cognitive_complexity/utils.rs @@ -1,5 +1,19 @@ use rustpython_parser::ast::{self, Stmt}; +pub fn get_repo_name(url: &str) -> String { + let url = url.trim_end_matches('/'); + + let repo_name = url.split('/').last().unwrap(); + + let repo_name = if repo_name.ends_with(".git") { + &repo_name[..repo_name.len() - 4] + } else { + repo_name + }; + + repo_name.to_string() +} + pub fn has_recursive_calls(statement: Stmt) -> bool { let mut ans = false;