Skip to content

Commit

Permalink
Merge pull request #21 from rohaquinlop/issue-15
Browse files Browse the repository at this point in the history
feat(build|docs): #15 add function level complexity analysis
  • Loading branch information
rohaquinlop authored Mar 6, 2024
2 parents a175c28 + 44c00af commit ce4e6ac
Show file tree
Hide file tree
Showing 13 changed files with 416 additions and 132 deletions.
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ name = "complexipy"
crate-type = ["cdylib"]

[dependencies]
csv = "1.3.0"
env_logger = "0.11.1"
ignore = "0.4.22"
log = "0.4.20"
Expand Down
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,31 @@ complexipy path/to/file.py -c 20 # Use the -c option to set the maximum cong
complexipy path/to/directory -c 0 # Set the maximum cognitive complexity to 0 to disable the exit with error
complexipy path/to/directory -o # Use the -o option to output the results to a CSV file, default is False
complexipy path/to/directory -d low # Use the -d option to set detail level, default is "normal". If set to "low" will show only files with complexity greater than the maximum complexity
complexipy path/to/directory -l file # Use the -l option to set the level of measurement, default is "function". If set to "file" will measure the complexity of the file and will validate the maximum complexity according to the file complexity.
```

If the cognitive complexity of a file is greater than the maximum cognitive, then
the return code will be 1 and exit with error, otherwise it will be 0.
### Options

- `-c` or `--max-complexity`: Set the maximum cognitive complexity, default is 15.
If the cognitive complexity of a file is greater than the maximum cognitive,
then the return code will be 1 and exit with error, otherwise it will be 0.
If set to 0, the exit with error will be disabled.
- `-o` or `--output`: Output the results to a CSV file, default is False. The
filename will be `complexipy.csv` and will be saved in the invocation directory.
- `-d` or `--detail`: Set the detail level, default is "normal". If set to "low"
will show only files or functions with complexity greater than the maximum
complexity.
- `-l` or `--level` Set the level of measurement, default is "function". If set
to "file" will measure the complexity of the file and will validate the maximum
complexity according to the file complexity. If set to "function" will measure
the complexity of the functions and will validate the maximum complexity
according to the function complexity. This option is useful if you want to set
a maximum complexity according for each file or for each function in the file
(or files).

If the cognitive complexity of a file or a function is greater than the maximum
cognitive cognitive complexity, then the return code will be 1 and exit with
error, otherwise it will be 0.

## Example

Expand All @@ -80,17 +101,18 @@ The cognitive complexity of the file is 1, and the output of the command
`complexipy path/to/file.py` will be:

```txt
─────────────────────── complexipy 0.2.2 🐙 ───────────────────────
- Finished analysis in test_decorator.py
──────────────────── 🎉 Analysis completed!🎉 ─────────────────────
Summary
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Path ┃ File ┃ Complexity ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ test_decorator.py │ test_decorator.py │ 1 │
└───────────────────┴───────────────────┴────────────┘
🧠 Total Cognitive Complexity in ./tests/test_decorator.py: 1
1 files analyzed in 0.0005 seconds
───────────────────────────── complexipy 0.3.0 🐙 ──────────────────────────────
Summary
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Path ┃ File ┃ Function ┃ Complexity ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ test_decorator.py │ test_decorator.py │ a_decorator │ 0 │
├───────────────────┼───────────────────┼─────────────┼────────────┤
│ test_decorator.py │ test_decorator.py │ b_decorator │ 1 │
└───────────────────┴───────────────────┴─────────────┴────────────┘
🧠 Total Cognitive Complexity in ./tests/src/test_decorator.py: 1
1 file analyzed in 0.0032 seconds
────────────────────────── 🎉 Analysis completed! 🎉 ───────────────────────────
```

#### Output to a CSV file
Expand All @@ -109,8 +131,9 @@ $ complexipy path/to/file.py -o
The output will be:

```csv
Path,File Name,Cognitive Complexity
test_decorator.py,test_decorator.py,1
Path,File Name,Function Name,Cognitive Complexity
test_decorator.py,test_decorator.py,a_decorator,0
test_decorator.py,test_decorator.py,b_decorator,1
```

### Analyzing a directory
Expand Down Expand Up @@ -140,7 +163,7 @@ $ complexipy https://github.com/rohaquinlop/complexipy -o

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
This project is licensed under the MIT License - see the [LICENSE](https://github.com/rohaquinlop/complexipy/blob/main/LICENSE) file
for details.

## Acknowledgments
Expand Down
102 changes: 55 additions & 47 deletions complexipy/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
from pathlib import Path
from complexipy import rust
import csv
from enum import Enum
from .types import (
DetailTypes,
Level,
)
from .utils import (
create_table_file_level,
create_table_function_level,
)
from complexipy import (
rust,
)
from complexipy.rust import (
FileComplexity,
)
import os
from pathlib import (
Path,
)
import re
from rich.console import Console
from rich.table import Table
from rich.align import (
Align,
)
from rich.console import (
Console,
)
import time
import typer

Expand All @@ -15,11 +32,6 @@
version = "0.3.0"


class DetailTypes(Enum):
low = "low" # Show only files with complexity above the max_complexity
normal = "normal" # Show all files with their complexity


@app.command()
def main(
path: str = typer.Argument(
Expand All @@ -29,7 +41,7 @@ def main(
15,
"--max-complexity",
"-c",
help="The maximum complexity allowed per file, set this value as 0 to set it as unlimited.",
help="The maximum complexity allowed per file, set this value as 0 to set it as unlimited. Default is 15.",
),
output: bool = typer.Option(
False, "--output", "-o", help="Output the results to a CSV file."
Expand All @@ -38,58 +50,54 @@ def main(
DetailTypes.normal.value,
"--details",
"-d",
help="Specify how detailed should be output.",
help="Specify how detailed should be output, it can be 'low' or 'normal'. Default is 'normal'.",
),
level: Level = typer.Option(
Level.function.value,
"--level",
"-l",
help="Specify the level of measurement, it can be 'function' or 'file'. Default is 'function'.",
),
):
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()
file_level = level == Level.file

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, is_url, max_complexity)
files: list[FileComplexity] = rust.main(
path, is_dir, is_url, max_complexity, file_level
)
execution_time = time.time() - start_time
console.rule(":tada: Analysis completed!:tada:")
output_csv_path = f"{invocation_path}/complexipy.csv"

if output:
with open(f"{invocation_path}/complexipy.csv", "w", newline="") as file:
writer = csv.writer(file)
writer.writerow(["Path", "File Name", "Cognitive Complexity"])
for file in files:
writer.writerow([file.path, file.file_name, file.complexity])
console.print(f"Results saved to {invocation_path}/complexipy.csv")
if output and file_level:
rust.output_csv_file_level(output_csv_path, files)
console.print(f"Results saved in {output_csv_path}")
if output and not file_level:
rust.output_csv_function_level(output_csv_path, files)
console.print(f"Results saved in {output_csv_path}")

# Summary
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")
total_complexity = 0
for file in files:
total_complexity += file.complexity
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]",
)
has_success = False
elif details != DetailTypes.low or max_complexity == 0:
table.add_row(
f"{file.path}",
f"[green]{file.file_name}[/green]",
f"[blue]{file.complexity}[/blue]",
)
console.print(table)
if file_level:
table, has_success, total_complexity = create_table_file_level(
files, max_complexity, details
)
else:
table, has_success, total_complexity = create_table_function_level(
files, max_complexity, details
)
console.print(Align.center(table))
console.print(f":brain: Total Cognitive Complexity in {path}: {total_complexity}")
console.print(f"{len(files)} files analyzed in {execution_time:.4f} seconds")
console.print(
f"{len(files)} file{'s' if len(files)> 1 else ''} analyzed in {execution_time:.4f} seconds"
)
console.rule(":tada: Analysis completed! :tada:")

if not has_success:
raise typer.Exit(code=1)
Expand Down
11 changes: 11 additions & 0 deletions complexipy/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from enum import Enum


class DetailTypes(Enum):
low = "low" # Show only files with complexity above the max_complexity
normal = "normal" # Show all files with their complexity


class Level(Enum):
function = "function"
file = "file"
72 changes: 72 additions & 0 deletions complexipy/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from .types import (
DetailTypes,
)
from complexipy.rust import (
FileComplexity,
)
from rich.table import Table


def create_table_file_level(
files: list[FileComplexity], max_complexity: int, details: DetailTypes
) -> tuple[Table, bool, int]:
has_success = 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")
total_complexity = 0
for file in files:
total_complexity += file.complexity
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]",
)
has_success = False
elif details != DetailTypes.low or max_complexity == 0:
table.add_row(
f"{file.path}",
f"[green]{file.file_name}[/green]",
f"[blue]{file.complexity}[/blue]",
)
return table, has_success, total_complexity


def create_table_function_level(
files: list[FileComplexity], complexity: int, details: DetailTypes
) -> tuple[Table, bool, int]:
has_success = 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("Function")
table.add_column("Complexity")
total_complexity = 0
for file in files:
total_complexity += file.complexity
for function in file.functions:
total_complexity += function.complexity
if function.complexity > complexity and complexity != 0:
table.add_row(
f"{file.path}",
f"[green]{file.file_name}[/green]",
f"[green]{function.name}[/green]",
f"[red]{function.complexity}[/red]",
)
has_success = False
elif details != DetailTypes.low or complexity == 0:
table.add_row(
f"{file.path}",
f"[green]{file.file_name}[/green]",
f"[green]{function.name}[/green]",
f"[blue]{function.complexity}[/blue]",
)
return table, has_success, total_complexity
Loading

0 comments on commit ce4e6ac

Please sign in to comment.