codecoco 3.5.1__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.
- codecoco-3.5.1/LICENSE +22 -0
- codecoco-3.5.1/PKG-INFO +278 -0
- codecoco-3.5.1/README.md +243 -0
- codecoco-3.5.1/codecoco.egg-info/PKG-INFO +278 -0
- codecoco-3.5.1/codecoco.egg-info/SOURCES.txt +27 -0
- codecoco-3.5.1/codecoco.egg-info/dependency_links.txt +1 -0
- codecoco-3.5.1/codecoco.egg-info/entry_points.txt +2 -0
- codecoco-3.5.1/codecoco.egg-info/not-zip-safe +1 -0
- codecoco-3.5.1/codecoco.egg-info/top_level.txt +1 -0
- codecoco-3.5.1/cognitive_complexity/__init__.py +1 -0
- codecoco-3.5.1/cognitive_complexity/api.py +174 -0
- codecoco-3.5.1/cognitive_complexity/autofix.py +238 -0
- codecoco-3.5.1/cognitive_complexity/cli.py +360 -0
- codecoco-3.5.1/cognitive_complexity/common_types.py +46 -0
- codecoco-3.5.1/cognitive_complexity/discovery.py +193 -0
- codecoco-3.5.1/cognitive_complexity/refactor.py +388 -0
- codecoco-3.5.1/cognitive_complexity/report.py +103 -0
- codecoco-3.5.1/cognitive_complexity/utils/__init__.py +0 -0
- codecoco-3.5.1/cognitive_complexity/utils/ast.py +165 -0
- codecoco-3.5.1/pyproject.toml +89 -0
- codecoco-3.5.1/setup.cfg +4 -0
- codecoco-3.5.1/setup.py +55 -0
- codecoco-3.5.1/tests/test_autofix.py +385 -0
- codecoco-3.5.1/tests/test_cli.py +690 -0
- codecoco-3.5.1/tests/test_cognitive_complexity.py +447 -0
- codecoco-3.5.1/tests/test_edge_cases.py +311 -0
- codecoco-3.5.1/tests/test_explain.py +377 -0
- codecoco-3.5.1/tests/test_properties.py +454 -0
- codecoco-3.5.1/tests/test_refactor.py +385 -0
codecoco-3.5.1/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Ilya Lebedev
|
|
4
|
+
Copyright (c) 2026 Mice Pápai
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
codecoco-3.5.1/PKG-INFO
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codecoco
|
|
3
|
+
Version: 3.5.1
|
|
4
|
+
Summary: Library and CLI to compute the cognitive complexity of Python functions
|
|
5
|
+
Home-page: https://github.com/qwhex/cococo
|
|
6
|
+
Author: Mice Pápai
|
|
7
|
+
Author-email: hello@micepapai.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: cognitive-complexity cli complexity cococo codecoco
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: author
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: description
|
|
28
|
+
Dynamic: description-content-type
|
|
29
|
+
Dynamic: home-page
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: requires-python
|
|
34
|
+
Dynamic: summary
|
|
35
|
+
|
|
36
|
+
# cococo
|
|
37
|
+
|
|
38
|
+
**co**de **co**gnitive **co**mplexity — a library and CLI to compute the
|
|
39
|
+
cognitive complexity of Python functions.
|
|
40
|
+
|
|
41
|
+
This is a fork of [Melevir/cognitive_complexity](https://github.com/Melevir/cognitive_complexity)
|
|
42
|
+
(MIT) that adds a command-line tool, modern-Python construct support, and
|
|
43
|
+
Python 3.10+ packaging. Three names differ on purpose: install **`codecoco`**,
|
|
44
|
+
import **`cognitive_complexity`**, run **`cococo`** (`cognitive_complexity` and
|
|
45
|
+
`cococo` were both already taken on PyPI, so the distribution is published as
|
|
46
|
+
`codecoco`).
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install codecoco
|
|
52
|
+
# or, with uv:
|
|
53
|
+
uv pip install codecoco
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This installs the `cococo` command and the importable `cognitive_complexity`
|
|
57
|
+
package. To install the unreleased tip from the repository instead:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install git+https://github.com/qwhex/cococo
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Command line
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cococo src/ # score every function, worst first
|
|
69
|
+
cococo src/ --max 20 # gate: exit non-zero if any function exceeds 20
|
|
70
|
+
cococo a.py b.py --min 10 # only show functions scoring >= 10
|
|
71
|
+
cococo src/ --max 20 --json # machine-readable report for a pipeline
|
|
72
|
+
cococo src/ --fix # apply safe guard-clause rewrites in place
|
|
73
|
+
cococo src/ --nested fold # pre-2.0.0 scoring: fold nested defs into the parent
|
|
74
|
+
cococo src/ --max 20 --baseline .cococo.json # ratchet: fail only on regressions
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`cococo` scores every function, method, and **named nested function** as its own
|
|
78
|
+
unit — a nested `def` is reported on its own row with a qualified name
|
|
79
|
+
(`outer.<locals>.inner`, `Klass.method.<locals>.helper`), scored from nesting
|
|
80
|
+
level 0, not folded into the function that encloses it. This keeps factory and
|
|
81
|
+
registry shapes (FastAPI/Flask app factories, decorator factories, dispatch
|
|
82
|
+
tables of closures) honest: the trivial outer function scores low and each inner
|
|
83
|
+
handler is judged on its own merits. Lambdas, being anonymous, still fold into
|
|
84
|
+
their enclosing function. See
|
|
85
|
+
[docs/nested-function-scoring.md](docs/nested-function-scoring.md) for the
|
|
86
|
+
rationale.
|
|
87
|
+
|
|
88
|
+
For gates pinned to pre-2.0.0 numbers, `--nested=fold` restores the old model
|
|
89
|
+
(nested defs fold into the enclosing function; a decorator/closure factory is
|
|
90
|
+
scored by its inner function) as a migration aid; the same is available in the
|
|
91
|
+
library as `get_cognitive_complexity(funcdef, fold_nested=True)`. See
|
|
92
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
93
|
+
|
|
94
|
+
### Refactor suggestions on a failing gate
|
|
95
|
+
|
|
96
|
+
When `--max` is exceeded, each offending function is reported on stderr together
|
|
97
|
+
with a few concrete, mechanical refactors and an estimated complexity drop — so
|
|
98
|
+
a human (or an agent) reading the failure knows what to do next:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
cococo: 1 function(s) exceed cognitive complexity 5
|
|
102
|
+
src/load.py:42 load = 14 (>5)
|
|
103
|
+
- Extract this block into a helper function (lines 50-61, ~-7 -> 7)
|
|
104
|
+
- Flatten nested block with a guard clause (lines 45-61, ~-3 -> 11) [--fix]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Suggestions tagged `[--fix]` can be applied automatically. `--fix` rewrites only
|
|
108
|
+
transforms it can prove keep behavior identical (an `if` with no `else` that is
|
|
109
|
+
the last statement of a function or loop body becomes an early
|
|
110
|
+
`return`/`continue` guard, de-indenting its body); anything else is left
|
|
111
|
+
untouched, and comments/formatting in the moved body are preserved.
|
|
112
|
+
|
|
113
|
+
### Adopting the gate incrementally
|
|
114
|
+
|
|
115
|
+
No real codebase passes a strict ceiling on day one, so the `--max` gate has two
|
|
116
|
+
ways to grandfather existing offenders rather than be all-or-nothing:
|
|
117
|
+
|
|
118
|
+
- **Per-function:** put `# cococo: ignore` on a function's `def` line to exclude
|
|
119
|
+
that one function from the gate (the listing still shows it). cococo warns when
|
|
120
|
+
an ignore is no longer needed (the function is back under the ceiling), so the
|
|
121
|
+
directives don't rot silently.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
def legacy_handler(req): # cococo: ignore
|
|
125
|
+
...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- **Whole codebase:** `--baseline FILE` (requires `--max`) records every current
|
|
129
|
+
score the first time it runs, then on later runs fails only on **regressions** —
|
|
130
|
+
a function rising above its recorded score, or new code over `--max`. This lets
|
|
131
|
+
a team adopt the gate against a dirty tree in one commit and ratchet down from
|
|
132
|
+
there. Commit the baseline file; delete it to re-baseline.
|
|
133
|
+
|
|
134
|
+
### Exit codes
|
|
135
|
+
|
|
136
|
+
In gate mode (`--max`), the exit code distinguishes the outcomes a CI step cares
|
|
137
|
+
about:
|
|
138
|
+
|
|
139
|
+
- **0** — all functions within the ceiling (or, without `--max`, a successful
|
|
140
|
+
listing).
|
|
141
|
+
- **1** — one or more functions exceed the ceiling (offenders printed with
|
|
142
|
+
suggestions).
|
|
143
|
+
- **2** — the gate could not be trusted: no functions were scanned (a typo'd or
|
|
144
|
+
empty path), a file was skipped (unreadable, unparseable, or too deeply nested
|
|
145
|
+
to score), or a `--fix` write failed. A `2` means "fix the setup", not "code is
|
|
146
|
+
too complex".
|
|
147
|
+
|
|
148
|
+
### JSON output
|
|
149
|
+
|
|
150
|
+
`--json` emits the same scores, per-construct breakdowns, and suggestions as a
|
|
151
|
+
single JSON document on stdout (exit code still gates on `--max`), so cococo
|
|
152
|
+
drops into a pipeline:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cococo src/ --max 20 --json | jq '.functions[] | select(.over)'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Library
|
|
159
|
+
|
|
160
|
+
Score every function under a path (no CLI required):
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from cognitive_complexity.discovery import scored_functions
|
|
164
|
+
|
|
165
|
+
for f in scored_functions(["src/"]):
|
|
166
|
+
print(f.qualname, f.score)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`scored_functions` returns a list of `ScoredFunction` named tuples with
|
|
170
|
+
`.score`, `.qualname`, `.path`, `.lineno`, `.funcdef`, `.breakdown`, and
|
|
171
|
+
`.ignored` fields.
|
|
172
|
+
|
|
173
|
+
The suggestion engine is importable too:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from cognitive_complexity.api import get_cognitive_complexity_breakdown
|
|
177
|
+
from cognitive_complexity.refactor import suggest_refactors
|
|
178
|
+
|
|
179
|
+
breakdown = get_cognitive_complexity_breakdown(funcdef)
|
|
180
|
+
for s in suggest_refactors(funcdef, breakdown):
|
|
181
|
+
print(s.title, s.estimated_reduction)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The low-level AST API:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
>>> import ast
|
|
188
|
+
>>> funcdef = ast.parse("""
|
|
189
|
+
... def f(a):
|
|
190
|
+
... return a * f(a - 1) # +1 for recursion
|
|
191
|
+
... """).body[0]
|
|
192
|
+
|
|
193
|
+
>>> from cognitive_complexity.api import get_cognitive_complexity
|
|
194
|
+
>>> get_cognitive_complexity(funcdef)
|
|
195
|
+
1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## What's different from upstream
|
|
199
|
+
|
|
200
|
+
This fork diverges from `Melevir/cognitive_complexity` 1.3.0:
|
|
201
|
+
|
|
202
|
+
- **`async for`** is counted as a loop (upstream scored it 0).
|
|
203
|
+
- **`match`/`case`** is counted as a single branching structure plus a nesting
|
|
204
|
+
level (upstream did not handle it).
|
|
205
|
+
- **comprehension `if` filters** each count as a decision point.
|
|
206
|
+
- **method recursion** (`self.method(...)` / `cls.method(...)`) is detected, not
|
|
207
|
+
only bare-name recursion.
|
|
208
|
+
- **named nested functions are scored as their own units** (reported as
|
|
209
|
+
`outer.<locals>.inner`), not folded into the enclosing function; lambdas still
|
|
210
|
+
fold. This removes the per-containment nesting surcharge on factory/registry
|
|
211
|
+
code and the old `is_decorator` special case — both still available via
|
|
212
|
+
`--nested=fold` for pre-2.0.0 compatibility. See
|
|
213
|
+
[docs/nested-function-scoring.md](docs/nested-function-scoring.md).
|
|
214
|
+
- a **`cococo` command-line interface**, with **heuristic refactor suggestions**
|
|
215
|
+
on a failing gate, a **`--json`** report for pipelines, and a **`--fix`** flag
|
|
216
|
+
that applies provably safe guard-clause rewrites.
|
|
217
|
+
- **Python 3.10+** only; type hints and packaging modernized.
|
|
218
|
+
|
|
219
|
+
The core control-flow scoring (Campbell's rules) is unchanged — it is the
|
|
220
|
+
empirically validated part of the metric.
|
|
221
|
+
|
|
222
|
+
## What is cognitive complexity
|
|
223
|
+
|
|
224
|
+
For a synthesis of the research and industry thinking on what makes code hard to
|
|
225
|
+
understand — and how cognitive complexity fits in — see
|
|
226
|
+
[docs/cognitive-complexity-of-code.md](docs/cognitive-complexity-of-code.md).
|
|
227
|
+
|
|
228
|
+
Here are some readings about cognitive complexity:
|
|
229
|
+
|
|
230
|
+
- [Cognitive Complexity, Because Testability != Understandability](https://blog.sonarsource.com/cognitive-complexity-because-testability-understandability);
|
|
231
|
+
- [Cognitive Complexity: A new way of measuring understandability](https://www.sonarsource.com/docs/CognitiveComplexity.pdf),
|
|
232
|
+
white paper by G. Ann Campbell;
|
|
233
|
+
- [Cognitive Complexity: the New Guide to Refactoring for Maintainable Code](https://www.youtube.com/watch?v=5C6AGTlKSjY);
|
|
234
|
+
- [Cognitive Complexity](https://docs.codeclimate.com/docs/cognitive-complexity)
|
|
235
|
+
from CodeClimate docs;
|
|
236
|
+
- [Is Your Code Readable By Humans? Cognitive Complexity Tells You](https://www.tomasvotruba.cz/blog/2018/05/21/is-your-code-readable-by-humans-cognitive-complexity-tells-you/).
|
|
237
|
+
|
|
238
|
+
## Realization details
|
|
239
|
+
|
|
240
|
+
This is not a precise realization of the original algorithm proposed by
|
|
241
|
+
[G. Ann Campbell](https://github.com/ganncamp), but it gives rather similar
|
|
242
|
+
results. The algorithm gives complexity points for breaking control flow,
|
|
243
|
+
nesting, recursion, and stacked logical operations.
|
|
244
|
+
|
|
245
|
+
**Known limitation:** only *direct* recursion is detected (a function calling
|
|
246
|
+
itself by name, or via `self`/`cls`). Indirect/mutual recursion — `a()` calls
|
|
247
|
+
`b()` calls `a()` — is not counted, since detecting it needs a whole-program
|
|
248
|
+
call graph rather than the single-function AST this tool works from.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
To develop `cococo`, first set up and activate a virtual environment so the toolchain (`python`, `pytest`, etc.) is available on your PATH:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# With standard pip/venv:
|
|
256
|
+
python -m venv .venv
|
|
257
|
+
source .venv/bin/activate
|
|
258
|
+
pip install -r requirements_dev.txt
|
|
259
|
+
|
|
260
|
+
# Or with uv:
|
|
261
|
+
uv venv
|
|
262
|
+
uv pip install -r requirements_dev.txt
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Once the environment is active (or by prefixing commands with `uv run`, e.g., `uv run just test`), you can use the `just` recipes:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
just install-hooks # pre-push runs `just check` (the same gate as CI)
|
|
269
|
+
just check # format-check + lint + type-check + complexity + tests + readme lint
|
|
270
|
+
just test # tests with coverage
|
|
271
|
+
just bench # performance benchmark
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`just check` is the single gate — CI runs the exact same recipe.
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT. See [LICENSE](LICENSE). Original work © Ilya Lebedev and contributors.
|
codecoco-3.5.1/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# cococo
|
|
2
|
+
|
|
3
|
+
**co**de **co**gnitive **co**mplexity — a library and CLI to compute the
|
|
4
|
+
cognitive complexity of Python functions.
|
|
5
|
+
|
|
6
|
+
This is a fork of [Melevir/cognitive_complexity](https://github.com/Melevir/cognitive_complexity)
|
|
7
|
+
(MIT) that adds a command-line tool, modern-Python construct support, and
|
|
8
|
+
Python 3.10+ packaging. Three names differ on purpose: install **`codecoco`**,
|
|
9
|
+
import **`cognitive_complexity`**, run **`cococo`** (`cognitive_complexity` and
|
|
10
|
+
`cococo` were both already taken on PyPI, so the distribution is published as
|
|
11
|
+
`codecoco`).
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install codecoco
|
|
17
|
+
# or, with uv:
|
|
18
|
+
uv pip install codecoco
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This installs the `cococo` command and the importable `cognitive_complexity`
|
|
22
|
+
package. To install the unreleased tip from the repository instead:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install git+https://github.com/qwhex/cococo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Command line
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cococo src/ # score every function, worst first
|
|
34
|
+
cococo src/ --max 20 # gate: exit non-zero if any function exceeds 20
|
|
35
|
+
cococo a.py b.py --min 10 # only show functions scoring >= 10
|
|
36
|
+
cococo src/ --max 20 --json # machine-readable report for a pipeline
|
|
37
|
+
cococo src/ --fix # apply safe guard-clause rewrites in place
|
|
38
|
+
cococo src/ --nested fold # pre-2.0.0 scoring: fold nested defs into the parent
|
|
39
|
+
cococo src/ --max 20 --baseline .cococo.json # ratchet: fail only on regressions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`cococo` scores every function, method, and **named nested function** as its own
|
|
43
|
+
unit — a nested `def` is reported on its own row with a qualified name
|
|
44
|
+
(`outer.<locals>.inner`, `Klass.method.<locals>.helper`), scored from nesting
|
|
45
|
+
level 0, not folded into the function that encloses it. This keeps factory and
|
|
46
|
+
registry shapes (FastAPI/Flask app factories, decorator factories, dispatch
|
|
47
|
+
tables of closures) honest: the trivial outer function scores low and each inner
|
|
48
|
+
handler is judged on its own merits. Lambdas, being anonymous, still fold into
|
|
49
|
+
their enclosing function. See
|
|
50
|
+
[docs/nested-function-scoring.md](docs/nested-function-scoring.md) for the
|
|
51
|
+
rationale.
|
|
52
|
+
|
|
53
|
+
For gates pinned to pre-2.0.0 numbers, `--nested=fold` restores the old model
|
|
54
|
+
(nested defs fold into the enclosing function; a decorator/closure factory is
|
|
55
|
+
scored by its inner function) as a migration aid; the same is available in the
|
|
56
|
+
library as `get_cognitive_complexity(funcdef, fold_nested=True)`. See
|
|
57
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
58
|
+
|
|
59
|
+
### Refactor suggestions on a failing gate
|
|
60
|
+
|
|
61
|
+
When `--max` is exceeded, each offending function is reported on stderr together
|
|
62
|
+
with a few concrete, mechanical refactors and an estimated complexity drop — so
|
|
63
|
+
a human (or an agent) reading the failure knows what to do next:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
cococo: 1 function(s) exceed cognitive complexity 5
|
|
67
|
+
src/load.py:42 load = 14 (>5)
|
|
68
|
+
- Extract this block into a helper function (lines 50-61, ~-7 -> 7)
|
|
69
|
+
- Flatten nested block with a guard clause (lines 45-61, ~-3 -> 11) [--fix]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Suggestions tagged `[--fix]` can be applied automatically. `--fix` rewrites only
|
|
73
|
+
transforms it can prove keep behavior identical (an `if` with no `else` that is
|
|
74
|
+
the last statement of a function or loop body becomes an early
|
|
75
|
+
`return`/`continue` guard, de-indenting its body); anything else is left
|
|
76
|
+
untouched, and comments/formatting in the moved body are preserved.
|
|
77
|
+
|
|
78
|
+
### Adopting the gate incrementally
|
|
79
|
+
|
|
80
|
+
No real codebase passes a strict ceiling on day one, so the `--max` gate has two
|
|
81
|
+
ways to grandfather existing offenders rather than be all-or-nothing:
|
|
82
|
+
|
|
83
|
+
- **Per-function:** put `# cococo: ignore` on a function's `def` line to exclude
|
|
84
|
+
that one function from the gate (the listing still shows it). cococo warns when
|
|
85
|
+
an ignore is no longer needed (the function is back under the ceiling), so the
|
|
86
|
+
directives don't rot silently.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def legacy_handler(req): # cococo: ignore
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **Whole codebase:** `--baseline FILE` (requires `--max`) records every current
|
|
94
|
+
score the first time it runs, then on later runs fails only on **regressions** —
|
|
95
|
+
a function rising above its recorded score, or new code over `--max`. This lets
|
|
96
|
+
a team adopt the gate against a dirty tree in one commit and ratchet down from
|
|
97
|
+
there. Commit the baseline file; delete it to re-baseline.
|
|
98
|
+
|
|
99
|
+
### Exit codes
|
|
100
|
+
|
|
101
|
+
In gate mode (`--max`), the exit code distinguishes the outcomes a CI step cares
|
|
102
|
+
about:
|
|
103
|
+
|
|
104
|
+
- **0** — all functions within the ceiling (or, without `--max`, a successful
|
|
105
|
+
listing).
|
|
106
|
+
- **1** — one or more functions exceed the ceiling (offenders printed with
|
|
107
|
+
suggestions).
|
|
108
|
+
- **2** — the gate could not be trusted: no functions were scanned (a typo'd or
|
|
109
|
+
empty path), a file was skipped (unreadable, unparseable, or too deeply nested
|
|
110
|
+
to score), or a `--fix` write failed. A `2` means "fix the setup", not "code is
|
|
111
|
+
too complex".
|
|
112
|
+
|
|
113
|
+
### JSON output
|
|
114
|
+
|
|
115
|
+
`--json` emits the same scores, per-construct breakdowns, and suggestions as a
|
|
116
|
+
single JSON document on stdout (exit code still gates on `--max`), so cococo
|
|
117
|
+
drops into a pipeline:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cococo src/ --max 20 --json | jq '.functions[] | select(.over)'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Library
|
|
124
|
+
|
|
125
|
+
Score every function under a path (no CLI required):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from cognitive_complexity.discovery import scored_functions
|
|
129
|
+
|
|
130
|
+
for f in scored_functions(["src/"]):
|
|
131
|
+
print(f.qualname, f.score)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`scored_functions` returns a list of `ScoredFunction` named tuples with
|
|
135
|
+
`.score`, `.qualname`, `.path`, `.lineno`, `.funcdef`, `.breakdown`, and
|
|
136
|
+
`.ignored` fields.
|
|
137
|
+
|
|
138
|
+
The suggestion engine is importable too:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from cognitive_complexity.api import get_cognitive_complexity_breakdown
|
|
142
|
+
from cognitive_complexity.refactor import suggest_refactors
|
|
143
|
+
|
|
144
|
+
breakdown = get_cognitive_complexity_breakdown(funcdef)
|
|
145
|
+
for s in suggest_refactors(funcdef, breakdown):
|
|
146
|
+
print(s.title, s.estimated_reduction)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The low-level AST API:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
>>> import ast
|
|
153
|
+
>>> funcdef = ast.parse("""
|
|
154
|
+
... def f(a):
|
|
155
|
+
... return a * f(a - 1) # +1 for recursion
|
|
156
|
+
... """).body[0]
|
|
157
|
+
|
|
158
|
+
>>> from cognitive_complexity.api import get_cognitive_complexity
|
|
159
|
+
>>> get_cognitive_complexity(funcdef)
|
|
160
|
+
1
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## What's different from upstream
|
|
164
|
+
|
|
165
|
+
This fork diverges from `Melevir/cognitive_complexity` 1.3.0:
|
|
166
|
+
|
|
167
|
+
- **`async for`** is counted as a loop (upstream scored it 0).
|
|
168
|
+
- **`match`/`case`** is counted as a single branching structure plus a nesting
|
|
169
|
+
level (upstream did not handle it).
|
|
170
|
+
- **comprehension `if` filters** each count as a decision point.
|
|
171
|
+
- **method recursion** (`self.method(...)` / `cls.method(...)`) is detected, not
|
|
172
|
+
only bare-name recursion.
|
|
173
|
+
- **named nested functions are scored as their own units** (reported as
|
|
174
|
+
`outer.<locals>.inner`), not folded into the enclosing function; lambdas still
|
|
175
|
+
fold. This removes the per-containment nesting surcharge on factory/registry
|
|
176
|
+
code and the old `is_decorator` special case — both still available via
|
|
177
|
+
`--nested=fold` for pre-2.0.0 compatibility. See
|
|
178
|
+
[docs/nested-function-scoring.md](docs/nested-function-scoring.md).
|
|
179
|
+
- a **`cococo` command-line interface**, with **heuristic refactor suggestions**
|
|
180
|
+
on a failing gate, a **`--json`** report for pipelines, and a **`--fix`** flag
|
|
181
|
+
that applies provably safe guard-clause rewrites.
|
|
182
|
+
- **Python 3.10+** only; type hints and packaging modernized.
|
|
183
|
+
|
|
184
|
+
The core control-flow scoring (Campbell's rules) is unchanged — it is the
|
|
185
|
+
empirically validated part of the metric.
|
|
186
|
+
|
|
187
|
+
## What is cognitive complexity
|
|
188
|
+
|
|
189
|
+
For a synthesis of the research and industry thinking on what makes code hard to
|
|
190
|
+
understand — and how cognitive complexity fits in — see
|
|
191
|
+
[docs/cognitive-complexity-of-code.md](docs/cognitive-complexity-of-code.md).
|
|
192
|
+
|
|
193
|
+
Here are some readings about cognitive complexity:
|
|
194
|
+
|
|
195
|
+
- [Cognitive Complexity, Because Testability != Understandability](https://blog.sonarsource.com/cognitive-complexity-because-testability-understandability);
|
|
196
|
+
- [Cognitive Complexity: A new way of measuring understandability](https://www.sonarsource.com/docs/CognitiveComplexity.pdf),
|
|
197
|
+
white paper by G. Ann Campbell;
|
|
198
|
+
- [Cognitive Complexity: the New Guide to Refactoring for Maintainable Code](https://www.youtube.com/watch?v=5C6AGTlKSjY);
|
|
199
|
+
- [Cognitive Complexity](https://docs.codeclimate.com/docs/cognitive-complexity)
|
|
200
|
+
from CodeClimate docs;
|
|
201
|
+
- [Is Your Code Readable By Humans? Cognitive Complexity Tells You](https://www.tomasvotruba.cz/blog/2018/05/21/is-your-code-readable-by-humans-cognitive-complexity-tells-you/).
|
|
202
|
+
|
|
203
|
+
## Realization details
|
|
204
|
+
|
|
205
|
+
This is not a precise realization of the original algorithm proposed by
|
|
206
|
+
[G. Ann Campbell](https://github.com/ganncamp), but it gives rather similar
|
|
207
|
+
results. The algorithm gives complexity points for breaking control flow,
|
|
208
|
+
nesting, recursion, and stacked logical operations.
|
|
209
|
+
|
|
210
|
+
**Known limitation:** only *direct* recursion is detected (a function calling
|
|
211
|
+
itself by name, or via `self`/`cls`). Indirect/mutual recursion — `a()` calls
|
|
212
|
+
`b()` calls `a()` — is not counted, since detecting it needs a whole-program
|
|
213
|
+
call graph rather than the single-function AST this tool works from.
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
To develop `cococo`, first set up and activate a virtual environment so the toolchain (`python`, `pytest`, etc.) is available on your PATH:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# With standard pip/venv:
|
|
221
|
+
python -m venv .venv
|
|
222
|
+
source .venv/bin/activate
|
|
223
|
+
pip install -r requirements_dev.txt
|
|
224
|
+
|
|
225
|
+
# Or with uv:
|
|
226
|
+
uv venv
|
|
227
|
+
uv pip install -r requirements_dev.txt
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Once the environment is active (or by prefixing commands with `uv run`, e.g., `uv run just test`), you can use the `just` recipes:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
just install-hooks # pre-push runs `just check` (the same gate as CI)
|
|
234
|
+
just check # format-check + lint + type-check + complexity + tests + readme lint
|
|
235
|
+
just test # tests with coverage
|
|
236
|
+
just bench # performance benchmark
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`just check` is the single gate — CI runs the exact same recipe.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT. See [LICENSE](LICENSE). Original work © Ilya Lebedev and contributors.
|