complexipy 4.1.0__tar.gz → 4.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {complexipy-4.1.0 → complexipy-4.2.0}/Cargo.lock +1 -1
- {complexipy-4.1.0 → complexipy-4.2.0}/Cargo.toml +1 -1
- {complexipy-4.1.0 → complexipy-4.2.0}/PKG-INFO +32 -4
- {complexipy-4.1.0 → complexipy-4.2.0}/README.md +31 -3
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/__init__.py +2 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/main.py +11 -1
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/utils.py +33 -7
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy.toml +2 -1
- {complexipy-4.1.0 → complexipy-4.2.0}/docs/index.md +27 -2
- {complexipy-4.1.0 → complexipy-4.2.0}/src/cognitive_complexity/mod.rs +117 -37
- {complexipy-4.1.0 → complexipy-4.2.0}/src/cognitive_complexity/utils.rs +25 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/uv.lock +192 -146
- {complexipy-4.1.0 → complexipy-4.2.0}/.pre-commit-config.yaml +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/LICENSE +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/build-wasm.sh +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/_complexipy.pyi +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/api.py +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/error_handlers.py +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/py.typed +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/complexipy/types.py +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/docs/img/complexipy_icon.svg +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/mkdocs.yml +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/pyproject.toml +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/serve-web-version.sh +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/src/classes/mod.rs +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/src/lib.rs +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/src/wasm.rs +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/.vscode-test.mjs +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/.vscodeignore +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/CHANGELOG.md +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/LICENSE +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/README.md +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/eslint.config.mjs +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/extension.js +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy.png +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy_icon.png +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/img/complexipy_icon.svg +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/jsconfig.json +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/package-lock.json +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/package.json +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/test/extension.test.js +0 -0
- {complexipy-4.1.0 → complexipy-4.2.0}/vscode/complexipy/test/statusbar.test.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: complexipy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -33,7 +33,7 @@ Project-URL: Repository, https://github.com/rohaquinlop/complexipy
|
|
|
33
33
|
# complexipy
|
|
34
34
|
|
|
35
35
|
<div align="center">
|
|
36
|
-
<img src="docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/rohaquinlop/complexipy/refs/heads/main/docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
|
|
37
37
|
|
|
38
38
|
<p><em>Blazingly fast cognitive complexity analysis for Python, written in Rust.</em></p>
|
|
39
39
|
|
|
@@ -86,6 +86,9 @@ complexipy . --max-complexity-allowed 10
|
|
|
86
86
|
|
|
87
87
|
# Save results to JSON/CSV
|
|
88
88
|
complexipy . --output-json --output-csv
|
|
89
|
+
|
|
90
|
+
# Analyze current directory while excluding specific files
|
|
91
|
+
complexipy . --exclude path/to/exclude.py --exclude path/to/other/exclude.py
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
### Python API
|
|
@@ -168,6 +171,7 @@ quiet = false
|
|
|
168
171
|
ignore-complexity = false
|
|
169
172
|
details = "normal"
|
|
170
173
|
sort = "asc"
|
|
174
|
+
exclude = []
|
|
171
175
|
|
|
172
176
|
[output]
|
|
173
177
|
csv = true
|
|
@@ -183,6 +187,7 @@ quiet = false
|
|
|
183
187
|
ignore-complexity = false
|
|
184
188
|
details = "normal"
|
|
185
189
|
sort = "asc"
|
|
190
|
+
exclude = []
|
|
186
191
|
|
|
187
192
|
[tool.complexipy.output]
|
|
188
193
|
csv = true
|
|
@@ -193,6 +198,7 @@ json = true
|
|
|
193
198
|
|
|
194
199
|
| Flag | Description | Default |
|
|
195
200
|
|------|-------------|---------|
|
|
201
|
+
| `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
|
|
196
202
|
| `--max-complexity-allowed` | Complexity threshold | `15` |
|
|
197
203
|
| `--output-json` | Save results as JSON | `false` |
|
|
198
204
|
| `--output-csv` | Save results as CSV | `false` |
|
|
@@ -202,6 +208,28 @@ json = true
|
|
|
202
208
|
| `--ignore-complexity` | Don't exit with error on threshold breach | `false` |
|
|
203
209
|
| `--version` | Show installed complexipy version and exit | - |
|
|
204
210
|
|
|
211
|
+
Example:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
# Exclude only top-level 'tests' directory under the provided root
|
|
215
|
+
complexipy . --exclude tests
|
|
216
|
+
# This will not exclude './complexipy/utils.py' if you pass '--exclude utils' at repo root,
|
|
217
|
+
# because there is no './utils' directory or file at that level.
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Inline Ignores
|
|
221
|
+
|
|
222
|
+
You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
|
|
226
|
+
if x and y:
|
|
227
|
+
return x + y
|
|
228
|
+
return 0
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
|
|
232
|
+
|
|
205
233
|
## API Reference
|
|
206
234
|
|
|
207
235
|
```python
|
|
@@ -226,11 +254,11 @@ FunctionComplexity:
|
|
|
226
254
|
|
|
227
255
|
<div align="center">
|
|
228
256
|
|
|
257
|
+
<sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource, G. Ann Campbell</sub>
|
|
258
|
+
|
|
229
259
|
**[Documentation](https://rohaquinlop.github.io/complexipy/) • [PyPI](https://pypi.org/project/complexipy/) • [GitHub](https://github.com/rohaquinlop/complexipy)**
|
|
230
260
|
|
|
231
261
|
<sub>Built with ❤️ by <a href="https://github.com/rohaquinlop">@rohaquinlop</a> and <a href="https://github.com/rohaquinlop/complexipy/graphs/contributors">contributors</a></sub>
|
|
232
262
|
|
|
233
|
-
<sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource</sub>
|
|
234
|
-
|
|
235
263
|
</div>
|
|
236
264
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# complexipy
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/rohaquinlop/complexipy/refs/heads/main/docs/img/complexipy_icon.svg" alt="complexipy" width="120" height="120">
|
|
5
5
|
|
|
6
6
|
<p><em>Blazingly fast cognitive complexity analysis for Python, written in Rust.</em></p>
|
|
7
7
|
|
|
@@ -54,6 +54,9 @@ complexipy . --max-complexity-allowed 10
|
|
|
54
54
|
|
|
55
55
|
# Save results to JSON/CSV
|
|
56
56
|
complexipy . --output-json --output-csv
|
|
57
|
+
|
|
58
|
+
# Analyze current directory while excluding specific files
|
|
59
|
+
complexipy . --exclude path/to/exclude.py --exclude path/to/other/exclude.py
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
### Python API
|
|
@@ -136,6 +139,7 @@ quiet = false
|
|
|
136
139
|
ignore-complexity = false
|
|
137
140
|
details = "normal"
|
|
138
141
|
sort = "asc"
|
|
142
|
+
exclude = []
|
|
139
143
|
|
|
140
144
|
[output]
|
|
141
145
|
csv = true
|
|
@@ -151,6 +155,7 @@ quiet = false
|
|
|
151
155
|
ignore-complexity = false
|
|
152
156
|
details = "normal"
|
|
153
157
|
sort = "asc"
|
|
158
|
+
exclude = []
|
|
154
159
|
|
|
155
160
|
[tool.complexipy.output]
|
|
156
161
|
csv = true
|
|
@@ -161,6 +166,7 @@ json = true
|
|
|
161
166
|
|
|
162
167
|
| Flag | Description | Default |
|
|
163
168
|
|------|-------------|---------|
|
|
169
|
+
| `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
|
|
164
170
|
| `--max-complexity-allowed` | Complexity threshold | `15` |
|
|
165
171
|
| `--output-json` | Save results as JSON | `false` |
|
|
166
172
|
| `--output-csv` | Save results as CSV | `false` |
|
|
@@ -170,6 +176,28 @@ json = true
|
|
|
170
176
|
| `--ignore-complexity` | Don't exit with error on threshold breach | `false` |
|
|
171
177
|
| `--version` | Show installed complexipy version and exit | - |
|
|
172
178
|
|
|
179
|
+
Example:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
# Exclude only top-level 'tests' directory under the provided root
|
|
183
|
+
complexipy . --exclude tests
|
|
184
|
+
# This will not exclude './complexipy/utils.py' if you pass '--exclude utils' at repo root,
|
|
185
|
+
# because there is no './utils' directory or file at that level.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Inline Ignores
|
|
189
|
+
|
|
190
|
+
You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
|
|
194
|
+
if x and y:
|
|
195
|
+
return x + y
|
|
196
|
+
return 0
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
|
|
200
|
+
|
|
173
201
|
## API Reference
|
|
174
202
|
|
|
175
203
|
```python
|
|
@@ -194,10 +222,10 @@ FunctionComplexity:
|
|
|
194
222
|
|
|
195
223
|
<div align="center">
|
|
196
224
|
|
|
225
|
+
<sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource, G. Ann Campbell</sub>
|
|
226
|
+
|
|
197
227
|
**[Documentation](https://rohaquinlop.github.io/complexipy/) • [PyPI](https://pypi.org/project/complexipy/) • [GitHub](https://github.com/rohaquinlop/complexipy)**
|
|
198
228
|
|
|
199
229
|
<sub>Built with ❤️ by <a href="https://github.com/rohaquinlop">@rohaquinlop</a> and <a href="https://github.com/rohaquinlop/complexipy/graphs/contributors">contributors</a></sub>
|
|
200
230
|
|
|
201
|
-
<sub>Inspired by the <a href="https://www.sonarsource.com/resources/cognitive-complexity/">Cognitive Complexity</a> research by SonarSource</sub>
|
|
202
|
-
|
|
203
231
|
</div>
|
|
@@ -51,6 +51,12 @@ def main(
|
|
|
51
51
|
None,
|
|
52
52
|
help="Paths to the directories or files to analyze, it can be a local paths or a git repository URL.",
|
|
53
53
|
),
|
|
54
|
+
exclude: Optional[List[str]] = typer.Option(
|
|
55
|
+
None,
|
|
56
|
+
"--exclude",
|
|
57
|
+
"-e",
|
|
58
|
+
help="Paths to the directories or files to exclude.",
|
|
59
|
+
),
|
|
54
60
|
max_complexity_allowed: Optional[int] = typer.Option(
|
|
55
61
|
None,
|
|
56
62
|
"--max-complexity-allowed",
|
|
@@ -110,6 +116,7 @@ def main(
|
|
|
110
116
|
sort,
|
|
111
117
|
output_csv,
|
|
112
118
|
output_json,
|
|
119
|
+
exclude,
|
|
113
120
|
) = get_arguments_value(
|
|
114
121
|
TOML_CONFIG,
|
|
115
122
|
paths,
|
|
@@ -120,6 +127,7 @@ def main(
|
|
|
120
127
|
sort,
|
|
121
128
|
output_csv,
|
|
122
129
|
output_json,
|
|
130
|
+
exclude,
|
|
123
131
|
)
|
|
124
132
|
|
|
125
133
|
if not quiet:
|
|
@@ -128,7 +136,9 @@ def main(
|
|
|
128
136
|
else:
|
|
129
137
|
console.rule(":octopus: complexipy")
|
|
130
138
|
start_time = time.time()
|
|
131
|
-
result: Tuple[List[FileComplexity], List[str]] = _complexipy.main(
|
|
139
|
+
result: Tuple[List[FileComplexity], List[str]] = _complexipy.main(
|
|
140
|
+
paths, quiet, exclude
|
|
141
|
+
)
|
|
132
142
|
files_complexities, failed_paths = result
|
|
133
143
|
execution_time = time.time() - start_time
|
|
134
144
|
output_csv_path = f"{INVOCATION_PATH}/complexipy.csv"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import sys
|
|
3
5
|
from complexipy.types import DetailTypes, Sort, TOMLType
|
|
@@ -35,11 +37,32 @@ def get_brain_icon():
|
|
|
35
37
|
return ":brain:"
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
def load_values_from_toml_key(value:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
def load_values_from_toml_key(key: str, value: TOMLType) -> TOMLType:
|
|
41
|
+
"""Normalize TOML values to expected runtime types.
|
|
42
|
+
|
|
43
|
+
- Convert `details` and `sort` string values to their Enum variants.
|
|
44
|
+
- Ensure `paths` and `exclude` are lists when provided as strings.
|
|
45
|
+
"""
|
|
46
|
+
if key == "details":
|
|
47
|
+
# Accept either Enum or string in TOML
|
|
48
|
+
if isinstance(value, DetailTypes):
|
|
49
|
+
return value
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
return DetailTypes(value)
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
if key == "sort":
|
|
55
|
+
if isinstance(value, Sort):
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
return Sort(value)
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
if key in ("paths", "exclude"):
|
|
62
|
+
# Allow a single string, normalize to list for downstream processing
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
return [value]
|
|
65
|
+
return value
|
|
43
66
|
|
|
44
67
|
return value
|
|
45
68
|
|
|
@@ -69,7 +92,7 @@ def load_toml_config(invocation_path: str, config_file_name: str) -> TOMLType |
|
|
|
69
92
|
return None
|
|
70
93
|
|
|
71
94
|
for key, value in data.items():
|
|
72
|
-
data[key] = load_values_from_toml_key(value)
|
|
95
|
+
data[key] = load_values_from_toml_key(key, value)
|
|
73
96
|
|
|
74
97
|
return data
|
|
75
98
|
|
|
@@ -125,7 +148,8 @@ def get_arguments_value(
|
|
|
125
148
|
sort_arg: Sort | None,
|
|
126
149
|
output_csv: bool | None,
|
|
127
150
|
output_json: bool | None,
|
|
128
|
-
|
|
151
|
+
exclude: List[str] | None,
|
|
152
|
+
) -> Tuple[List[str], int, bool, bool, DetailTypes, Sort, bool, bool, List[str]]:
|
|
129
153
|
paths = get_argument_value(toml_config, "paths", paths)
|
|
130
154
|
max_complexity_allowed = get_argument_value(
|
|
131
155
|
toml_config, "max-complexity-allowed", max_complexity_allowed, 15
|
|
@@ -138,6 +162,7 @@ def get_arguments_value(
|
|
|
138
162
|
sort_arg = get_argument_value(toml_config, "sort", sort_arg, Sort.asc)
|
|
139
163
|
output_csv = get_argument_value(toml_config, "output-csv", output_csv, False)
|
|
140
164
|
output_json = get_argument_value(toml_config, "output-json", output_json, False)
|
|
165
|
+
exclude = get_argument_value(toml_config, "exclude", exclude, [])
|
|
141
166
|
|
|
142
167
|
return (
|
|
143
168
|
paths,
|
|
@@ -148,6 +173,7 @@ def get_arguments_value(
|
|
|
148
173
|
sort_arg,
|
|
149
174
|
output_csv,
|
|
150
175
|
output_json,
|
|
176
|
+
exclude,
|
|
151
177
|
)
|
|
152
178
|
|
|
153
179
|
|
|
@@ -67,6 +67,10 @@ complexipy . --max-complexity-allowed 10
|
|
|
67
67
|
|
|
68
68
|
# Generate reports
|
|
69
69
|
complexipy . --output-json --output-csv
|
|
70
|
+
|
|
71
|
+
# Analyze current directory while excluding specific files
|
|
72
|
+
complexipy . --exclude file_to_exclude.py --exclude path/to/other/exclude.py
|
|
73
|
+
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
### Python API
|
|
@@ -131,6 +135,7 @@ quiet = false
|
|
|
131
135
|
ignore-complexity = false
|
|
132
136
|
details = "normal"
|
|
133
137
|
sort = "asc"
|
|
138
|
+
exclude = []
|
|
134
139
|
|
|
135
140
|
[output]
|
|
136
141
|
csv = true
|
|
@@ -148,6 +153,7 @@ quiet = false
|
|
|
148
153
|
ignore-complexity = false
|
|
149
154
|
details = "normal"
|
|
150
155
|
sort = "asc"
|
|
156
|
+
exclude = []
|
|
151
157
|
|
|
152
158
|
[tool.complexipy.output]
|
|
153
159
|
csv = true
|
|
@@ -161,6 +167,7 @@ json = true
|
|
|
161
167
|
|
|
162
168
|
| Option | Description | Default |
|
|
163
169
|
|--------|-------------|---------|
|
|
170
|
+
| `--exclude` | Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored. | |
|
|
164
171
|
| `--max-complexity-allowed <N>` | Complexity threshold for warnings | `15` |
|
|
165
172
|
| `--output-json` | Export results to JSON | `false` |
|
|
166
173
|
| `--output-csv` | Export results to CSV | `false` |
|
|
@@ -181,11 +188,29 @@ complexipy . --max-complexity-allowed 8 --output-json
|
|
|
181
188
|
|
|
182
189
|
# Silent analysis sorted by name
|
|
183
190
|
complexipy src/ --sort name --quiet
|
|
191
|
+
|
|
192
|
+
# Exclude specific paths relative to root
|
|
193
|
+
complexipy . --exclude tests --exclude utils
|
|
194
|
+
# Note: this excludes only './tests/**' and './utils/**' under '.',
|
|
195
|
+
# it does not exclude './complexipy/utils.py'.
|
|
184
196
|
```
|
|
185
197
|
|
|
186
198
|
!!! tip "Exit Codes"
|
|
187
199
|
complexipy returns exit code `1` when functions exceed the complexity threshold. Use `--ignore-complexity` to suppress this behavior in CI/CD pipelines.
|
|
188
200
|
|
|
201
|
+
### Inline Ignores
|
|
202
|
+
|
|
203
|
+
You can explicitly ignore a known complex function inline, similar to Ruff's `C901` ignores:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
def legacy_adapter(x, y): # noqa: complexipy (safe wrapper)
|
|
207
|
+
if x and y:
|
|
208
|
+
return x + y
|
|
209
|
+
return 0
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Place `# noqa: complexipy` on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it’s ignored by the parser.
|
|
213
|
+
|
|
189
214
|
## API Reference
|
|
190
215
|
|
|
191
216
|
### Functions
|
|
@@ -366,7 +391,7 @@ def process_orders(orders): # Base: 0
|
|
|
366
391
|
```bash
|
|
367
392
|
$ complexipy example.py
|
|
368
393
|
|
|
369
|
-
───────────────────────────── 🐙 complexipy
|
|
394
|
+
───────────────────────────── 🐙 complexipy ──────────────────────────────
|
|
370
395
|
Summary
|
|
371
396
|
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
|
372
397
|
┃ Path ┃ File ┃ Function ┃ Complexity ┃
|
|
@@ -385,7 +410,7 @@ $ complexipy example.py
|
|
|
385
410
|
|
|
386
411
|
## Links & Resources
|
|
387
412
|
|
|
388
|
-
- **📚 [Research Paper](https://www.sonarsource.com/resources/cognitive-complexity/)** - Original cognitive complexity research by SonarSource
|
|
413
|
+
- **📚 [Research Paper](https://www.sonarsource.com/resources/cognitive-complexity/)** - Original cognitive complexity research by SonarSource, G. Ann Campbell
|
|
389
414
|
- **🐙 [GitHub Repository](https://github.com/rohaquinlop/complexipy)** - Source code and issue tracking
|
|
390
415
|
- **📦 [PyPI Package](https://pypi.org/project/complexipy/)** - Installation and version history
|
|
391
416
|
- **🛠️ [GitHub Action](https://github.com/marketplace/actions/complexipy)** - CI/CD integration
|
|
@@ -2,7 +2,7 @@ pub mod utils;
|
|
|
2
2
|
|
|
3
3
|
#[cfg(any(feature = "python", feature = "wasm"))]
|
|
4
4
|
mod shared_deps {
|
|
5
|
-
pub use super::utils::{count_bool_ops, get_line_number, is_decorator};
|
|
5
|
+
pub use super::utils::{count_bool_ops, get_line_number, has_noqa_complexipy, is_decorator};
|
|
6
6
|
pub use crate::classes::{CodeComplexity, FileComplexity, FunctionComplexity, LineComplexity};
|
|
7
7
|
pub use ruff_python_ast::{self as ast, Stmt};
|
|
8
8
|
}
|
|
@@ -37,28 +37,34 @@ type ComplexitiesAndFailedPaths = (Vec<FileComplexity>, Vec<String>);
|
|
|
37
37
|
|
|
38
38
|
#[cfg(feature = "python")]
|
|
39
39
|
#[pyfunction]
|
|
40
|
-
pub fn main(
|
|
40
|
+
pub fn main(
|
|
41
|
+
paths: Vec<&str>,
|
|
42
|
+
quiet: bool,
|
|
43
|
+
exclude: Vec<&str>,
|
|
44
|
+
) -> PyResult<ComplexitiesAndFailedPaths> {
|
|
41
45
|
let re = Regex::new(r"^(https:\/\/|http:\/\/|www\.|git@)(github|gitlab)\.com(\/[\w.-]+){2,}$")
|
|
42
46
|
.unwrap();
|
|
43
47
|
|
|
44
|
-
let all_files_paths: Vec<(&str, bool, bool, bool)> = paths
|
|
48
|
+
let all_files_paths: Vec<(&str, bool, bool, bool, Vec<&str>)> = paths
|
|
45
49
|
.iter()
|
|
46
50
|
.map(|&path| {
|
|
47
51
|
let is_url = re.is_match(path);
|
|
48
52
|
|
|
49
53
|
if is_url {
|
|
50
|
-
(path, false, true, quiet)
|
|
54
|
+
(path, false, true, quiet, exclude.clone())
|
|
51
55
|
} else if metadata(path).unwrap().is_dir() {
|
|
52
|
-
(path, true, false, quiet)
|
|
56
|
+
(path, true, false, quiet, exclude.clone())
|
|
53
57
|
} else {
|
|
54
|
-
(path, false, false, quiet)
|
|
58
|
+
(path, false, false, quiet, exclude.clone())
|
|
55
59
|
}
|
|
56
60
|
})
|
|
57
61
|
.collect();
|
|
58
62
|
|
|
59
63
|
let all_files_processed: Vec<Result<ComplexitiesAndFailedPaths, PyErr>> = all_files_paths
|
|
60
64
|
.iter()
|
|
61
|
-
.map(|(path, is_dir, is_url, quiet)|
|
|
65
|
+
.map(|(path, is_dir, is_url, quiet, exclude)| {
|
|
66
|
+
process_path(path, *is_dir, *is_url, *quiet, exclude.clone())
|
|
67
|
+
})
|
|
62
68
|
.collect();
|
|
63
69
|
|
|
64
70
|
let mut successful = Vec::new();
|
|
@@ -83,6 +89,7 @@ pub fn process_path(
|
|
|
83
89
|
is_dir: bool,
|
|
84
90
|
is_url: bool,
|
|
85
91
|
quiet: bool,
|
|
92
|
+
exclude: Vec<&str>,
|
|
86
93
|
) -> Result<ComplexitiesAndFailedPaths, PyErr> {
|
|
87
94
|
let mut file_complexities = Vec::new();
|
|
88
95
|
let mut failed_paths = Vec::new();
|
|
@@ -121,13 +128,13 @@ pub fn process_path(
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
let repo_path = dir.path().join(&repo_name).to_str().unwrap().to_string();
|
|
124
|
-
let (complexities, f_paths) = evaluate_dir(&repo_path, quiet);
|
|
131
|
+
let (complexities, f_paths) = evaluate_dir(&repo_path, quiet, exclude.clone());
|
|
125
132
|
dir.close()?;
|
|
126
133
|
|
|
127
134
|
file_complexities = complexities;
|
|
128
135
|
failed_paths = f_paths;
|
|
129
136
|
} else if is_dir {
|
|
130
|
-
let (complexities, f_paths) = evaluate_dir(path, quiet);
|
|
137
|
+
let (complexities, f_paths) = evaluate_dir(path, quiet, exclude.clone());
|
|
131
138
|
file_complexities = complexities;
|
|
132
139
|
failed_paths = f_paths;
|
|
133
140
|
} else {
|
|
@@ -149,18 +156,85 @@ pub fn process_path(
|
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
#[cfg(feature = "python")]
|
|
152
|
-
fn evaluate_dir(path: &str, quiet: bool) -> ComplexitiesAndFailedPaths {
|
|
159
|
+
fn evaluate_dir(path: &str, quiet: bool, exclude: Vec<&str>) -> ComplexitiesAndFailedPaths {
|
|
153
160
|
let mut files_paths: Vec<String> = Vec::new();
|
|
154
161
|
|
|
155
162
|
let parent_dir = path::Path::new(path).parent().unwrap().to_str().unwrap();
|
|
156
163
|
|
|
164
|
+
// Build path-aware exclude specs, relative to the provided root `path`.
|
|
165
|
+
// Only exclude if the exclude entry resolves to an existing directory (prefix match)
|
|
166
|
+
// or file (exact match) under the root. Otherwise, ignore the entry.
|
|
167
|
+
#[derive(Clone)]
|
|
168
|
+
struct ExcludeSpec {
|
|
169
|
+
// canonical, normalized absolute path
|
|
170
|
+
abs: String,
|
|
171
|
+
is_dir: bool,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let root_canon = match path::Path::new(path).canonicalize() {
|
|
175
|
+
Ok(p) => p,
|
|
176
|
+
Err(_) => path::PathBuf::from(path),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let mut exclude_specs: Vec<ExcludeSpec> = Vec::new();
|
|
180
|
+
for raw in exclude.iter() {
|
|
181
|
+
let mut candidate: path::PathBuf;
|
|
182
|
+
let p = path::Path::new(raw);
|
|
183
|
+
if p.is_absolute() {
|
|
184
|
+
candidate = p.to_path_buf();
|
|
185
|
+
} else {
|
|
186
|
+
candidate = root_canon.join(p);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Resolve to canonical if possible
|
|
190
|
+
let (exists, is_dir, is_file, abs_str) = match candidate.canonicalize() {
|
|
191
|
+
Ok(canon) => {
|
|
192
|
+
let is_dir = canon.is_dir();
|
|
193
|
+
let is_file = canon.is_file();
|
|
194
|
+
let s = canon.to_string_lossy().replace('\\', "/");
|
|
195
|
+
(true, is_dir, is_file, s)
|
|
196
|
+
}
|
|
197
|
+
Err(_) => (false, false, false, String::new()),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if exists {
|
|
201
|
+
if is_dir || is_file {
|
|
202
|
+
exclude_specs.push(ExcludeSpec { abs: abs_str, is_dir });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// If it doesn't exist under the root, ignore the exclude entry
|
|
206
|
+
}
|
|
207
|
+
|
|
157
208
|
// Get all the python files in the directory
|
|
158
209
|
for entry in Walk::new(path) {
|
|
159
210
|
let entry = entry.unwrap();
|
|
160
|
-
let
|
|
211
|
+
let entry_path = entry.path();
|
|
161
212
|
|
|
162
|
-
if
|
|
163
|
-
|
|
213
|
+
if entry_path.extension().and_then(|s| s.to_str()) != Some("py") {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Determine if this file should be excluded using path-aware specs
|
|
218
|
+
let file_abs = match entry_path.canonicalize() {
|
|
219
|
+
Ok(p) => p,
|
|
220
|
+
Err(_) => entry_path.to_path_buf(),
|
|
221
|
+
};
|
|
222
|
+
let file_abs_str = file_abs.to_string_lossy().replace('\\', "/");
|
|
223
|
+
|
|
224
|
+
let is_excluded = exclude_specs.iter().any(|spec| {
|
|
225
|
+
if spec.is_dir {
|
|
226
|
+
// Directory prefix match
|
|
227
|
+
file_abs_str.starts_with(&spec.abs)
|
|
228
|
+
} else {
|
|
229
|
+
// Exact file match
|
|
230
|
+
file_abs_str == spec.abs
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if !is_excluded {
|
|
235
|
+
if let Some(file_path_str) = entry_path.to_str() {
|
|
236
|
+
files_paths.push(file_path_str.to_string());
|
|
237
|
+
}
|
|
164
238
|
}
|
|
165
239
|
}
|
|
166
240
|
|
|
@@ -289,34 +363,40 @@ pub fn function_level_cognitive_complexity_shared(
|
|
|
289
363
|
for node in ast_body.iter() {
|
|
290
364
|
match node {
|
|
291
365
|
Stmt::FunctionDef(f) => {
|
|
292
|
-
let (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
366
|
+
let start_line = get_line_number(usize::from(f.range.start()), code);
|
|
367
|
+
if !has_noqa_complexipy(start_line, code) {
|
|
368
|
+
let (func_complexity, line_complexities) =
|
|
369
|
+
statement_cognitive_complexity_shared(node, 0, code);
|
|
370
|
+
|
|
371
|
+
let function = FunctionComplexity {
|
|
372
|
+
name: f.name.to_string(),
|
|
373
|
+
complexity: func_complexity,
|
|
374
|
+
line_start: start_line,
|
|
375
|
+
line_end: get_line_number(usize::from(f.range.end()), code),
|
|
376
|
+
line_complexities,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
functions.push(function);
|
|
380
|
+
}
|
|
304
381
|
}
|
|
305
382
|
Stmt::ClassDef(c) => {
|
|
306
383
|
for node in c.body.iter() {
|
|
307
384
|
if let Stmt::FunctionDef(f) = node {
|
|
308
|
-
let (
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
385
|
+
let start_line = get_line_number(usize::from(f.range.start()), code);
|
|
386
|
+
if !has_noqa_complexipy(start_line, code) {
|
|
387
|
+
let (func_complexity, line_complexities) =
|
|
388
|
+
statement_cognitive_complexity_shared(node, 0, code);
|
|
389
|
+
|
|
390
|
+
let function = FunctionComplexity {
|
|
391
|
+
name: format!("{}::{}", c.name, f.name),
|
|
392
|
+
complexity: func_complexity,
|
|
393
|
+
line_start: start_line,
|
|
394
|
+
line_end: get_line_number(usize::from(f.range.end()), code),
|
|
395
|
+
line_complexities,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
functions.push(function);
|
|
399
|
+
}
|
|
320
400
|
}
|
|
321
401
|
}
|
|
322
402
|
}
|
|
@@ -241,3 +241,28 @@ pub fn get_line_number(byte_index: usize, code: &str) -> u64 {
|
|
|
241
241
|
let newline_count = before_slice.chars().filter(|&c| c == '\n').count();
|
|
242
242
|
(newline_count + 1) as u64
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
#[cfg(any(feature = "python", feature = "wasm"))]
|
|
246
|
+
pub fn has_noqa_complexipy(line_number: u64, code: &str) -> bool {
|
|
247
|
+
if line_number == 0 {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let lines: Vec<&str> = code.lines().collect();
|
|
252
|
+
let idx = (line_number as usize).saturating_sub(1);
|
|
253
|
+
|
|
254
|
+
let contains_marker = |s: &str| -> bool {
|
|
255
|
+
let lower = s.to_lowercase();
|
|
256
|
+
lower.contains("noqa: complexipy")
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if idx < lines.len() && contains_marker(lines[idx]) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if idx > 0 && contains_marker(lines[idx - 1]) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
false
|
|
268
|
+
}
|