codecoco 3.5.1__py3-none-any.whl

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.
@@ -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.
@@ -0,0 +1,16 @@
1
+ codecoco-3.5.1.dist-info/licenses/LICENSE,sha256=Lh0Xyp2No-APZG8c0516AgszsFXTAqp1rVcLJGw9gQ8,1100
2
+ cognitive_complexity/__init__.py,sha256=q84OxOhhZQRg9QLCpRzm8ZW7aGHtGUAnNgVlGvZ9vBg,22
3
+ cognitive_complexity/api.py,sha256=a9LDYIoBtHsrcdxIdMgO7bzhMyjppsq9xaRyeQOJ870,7338
4
+ cognitive_complexity/autofix.py,sha256=wlct5ISkUmZ1bMJ9V9IBiftDIPyy1aRuOwDih7NxlTI,9507
5
+ cognitive_complexity/cli.py,sha256=PjeVJvrZisMaXzLAHuM2EdZFbKNiw52J6PhSPHfcYzI,13600
6
+ cognitive_complexity/common_types.py,sha256=TLDrkhrL5WGh0AFzITPUG-YgX5jFPpu8aC3-vb7THcE,1417
7
+ cognitive_complexity/discovery.py,sha256=-BgOQBUt_Nq8F2I-7Z1DSmoWaW0MXPHGfJKG1ppHRYk,7831
8
+ cognitive_complexity/refactor.py,sha256=R0aC4pWKEq8qao42iTfz3b98dtAY5nzsmM8I1DZtRjU,13247
9
+ cognitive_complexity/report.py,sha256=LZq23V5akEsPWbUsY6Dqiy2mqyioRMrzo4d_1Zuxekg,3503
10
+ cognitive_complexity/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ cognitive_complexity/utils/ast.py,sha256=HnEMB7YPv-MhyqhZ3kDiYwGnL-IYcI1xm0iiY_yRvLk,6607
12
+ codecoco-3.5.1.dist-info/METADATA,sha256=WjYonysp9JrSFw0XWmcL6WO9Nl9UMwh6WE68HMOcM7k,11032
13
+ codecoco-3.5.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ codecoco-3.5.1.dist-info/entry_points.txt,sha256=enY0Xz9nOD6y2JVVhxC49eU6iotj1IutDk9r8_DTSeY,57
15
+ codecoco-3.5.1.dist-info/top_level.txt,sha256=smc8Vi5x-0c0B0ZelyGn3R_5AKaYfRDgQsjxfWWDuZ0,21
16
+ codecoco-3.5.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cococo = cognitive_complexity.cli:main
@@ -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.
@@ -0,0 +1 @@
1
+ cognitive_complexity
@@ -0,0 +1 @@
1
+ __version__ = "3.5.1"
@@ -0,0 +1,174 @@
1
+ import ast
2
+ from typing import NamedTuple
3
+
4
+ from cognitive_complexity.common_types import AnyFuncdef, is_funcdef
5
+ from cognitive_complexity.utils.ast import (
6
+ call_targets_name,
7
+ decorator_inner,
8
+ describe_node,
9
+ has_recursive_calls,
10
+ process_node_itself,
11
+ )
12
+
13
+
14
+ class Contribution(NamedTuple):
15
+ """One scored construct in a function's cognitive-complexity breakdown.
16
+
17
+ ``lineno`` is the source line, ``label`` names the construct (e.g. ``if``,
18
+ ``for``, ``bool-op``), and ``points`` is what this node alone added to the
19
+ score, so the sum of every contribution's ``points`` equals the function's
20
+ total complexity. ``nesting`` is the nesting depth in effect at this node.
21
+
22
+ For control-flow breakers (if/for/while/except/match/ternary) the nesting
23
+ penalty is *part of* ``points`` and ``nesting_counted`` is true, so the
24
+ structural cost is ``points - nesting``. For ``elif``/``else``, bool-ops and
25
+ comprehension filters ``nesting`` is only ambient context
26
+ (``nesting_counted`` false) and all of ``points`` is structural.
27
+ """
28
+
29
+ lineno: int
30
+ label: str
31
+ points: int
32
+ nesting: int
33
+ nesting_counted: bool
34
+
35
+
36
+ def get_cognitive_complexity(funcdef: AnyFuncdef, fold_nested: bool = False) -> int:
37
+ """Total cognitive complexity: the sum of every scored construct's points."""
38
+ return sum(c.points for c in get_cognitive_complexity_breakdown(funcdef, fold_nested))
39
+
40
+
41
+ def get_cognitive_complexity_breakdown(
42
+ funcdef: AnyFuncdef, fold_nested: bool = False
43
+ ) -> list[Contribution]:
44
+ """Per-node breakdown of a function's cognitive complexity.
45
+
46
+ Records every construct that contributed points and the nesting level in
47
+ effect at that point. Recursive calls add a trailing synthetic entry. The
48
+ ``points`` column sums to the total returned by
49
+ :func:`get_cognitive_complexity`. By default named nested functions are *not*
50
+ folded in — they are scored as their own units. With ``fold_nested=True``
51
+ (the pre-2.0.0 model) a nested def folds into its enclosing function as a
52
+ nesting level, and a decorator/closure factory is scored by its inner
53
+ function.
54
+ """
55
+ inner = decorator_inner(funcdef) if fold_nested else None
56
+ if inner is not None:
57
+ return get_cognitive_complexity_breakdown(inner, fold_nested)
58
+
59
+ contributions: list[Contribution] = []
60
+ # Detect direct recursion inline during this single walk instead of a second
61
+ # ast.walk. In unit mode the walk visits exactly the function's own scope, so
62
+ # ``rec_name`` is set and a self-call is found here. In fold mode the walk
63
+ # descends into folded nested defs, whose calls are NOT the outer's recursion,
64
+ # so inline detection is disabled and the own-scope-only ``has_recursive_calls``
65
+ # is used instead.
66
+ rec_name = None if fold_nested else funcdef.name
67
+ rec_found = [False]
68
+ for node in funcdef.body:
69
+ _collect_breakdown(node, 0, funcdef.lineno, contributions, fold_nested, rec_name, rec_found)
70
+ recursive = has_recursive_calls(funcdef) if fold_nested else rec_found[0]
71
+ if recursive:
72
+ contributions.append(Contribution(funcdef.lineno, "recursion", 1, 0, False))
73
+ return contributions
74
+
75
+
76
+ def _mark_recursion(node: ast.AST, rec_name: str | None, rec_found: list[bool]) -> None:
77
+ """Flag direct recursion when ``node`` is a call to ``rec_name`` (None disables)."""
78
+ if rec_name is not None and isinstance(node, ast.Call) and call_targets_name(node, rec_name):
79
+ rec_found[0] = True
80
+
81
+
82
+ def _collect_breakdown(
83
+ node: ast.AST,
84
+ increment_by: int,
85
+ parent_lineno: int,
86
+ out: list[Contribution],
87
+ fold_nested: bool,
88
+ rec_name: str | None,
89
+ rec_found: list[bool],
90
+ ) -> None:
91
+ _mark_recursion(node, rec_name, rec_found)
92
+ # In unit mode a named nested function is its own reporting unit (discovered
93
+ # separately by the CLI, scored from nesting 0): it contributes nothing here
94
+ # and the walk does not descend into it. In fold mode it is left to
95
+ # process_node_itself, which treats it as a nesting incrementer. Lambdas are
96
+ # anonymous and always fold.
97
+ if not fold_nested and is_funcdef(node):
98
+ return
99
+
100
+ # `if`/`elif`/`else` chains need their body and orelse scored at different
101
+ # nesting levels (the body nests one deeper; a trailing `elif` is a sibling
102
+ # at the same level), which the uniform child walk below cannot express —
103
+ # so they get their own handler.
104
+ if isinstance(node, ast.If):
105
+ _collect_if_breakdown(
106
+ node, increment_by, out, fold_nested, rec_name, rec_found, is_elif_arm=False
107
+ )
108
+ return
109
+
110
+ nesting_before = increment_by
111
+ increment_by, base_complexity, should_iter_children = process_node_itself(
112
+ node, increment_by, fold_nested
113
+ )
114
+ # Some scored nodes (ast.comprehension) carry no line of their own; fall
115
+ # back to the nearest ancestor that did.
116
+ lineno = getattr(node, "lineno", parent_lineno)
117
+
118
+ if base_complexity:
119
+ # Control-flow breakers bumped ``increment_by`` for their own body, so
120
+ # the level they sit at is the pre-bump value and their nesting penalty
121
+ # is baked into base_complexity. Bool-ops / comprehension filters don't
122
+ # bump, so they sit at the ambient level with no nesting penalty.
123
+ nesting_counted = increment_by != nesting_before
124
+ node_nesting = increment_by - 1 if nesting_counted else nesting_before
125
+ out.append(
126
+ Contribution(
127
+ lineno, describe_node(node), base_complexity, node_nesting, nesting_counted
128
+ )
129
+ )
130
+
131
+ if should_iter_children:
132
+ for child in ast.iter_child_nodes(node):
133
+ _collect_breakdown(child, increment_by, lineno, out, fold_nested, rec_name, rec_found)
134
+
135
+
136
+ def _collect_if_breakdown(
137
+ node: ast.If,
138
+ increment_by: int,
139
+ out: list[Contribution],
140
+ fold_nested: bool,
141
+ rec_name: str | None,
142
+ rec_found: list[bool],
143
+ *,
144
+ is_elif_arm: bool,
145
+ ) -> None:
146
+ # B1: +1 for the `if`/`elif` itself. B3: a nesting penalty applies to a
147
+ # leading `if` (the more deeply nested, the costlier) but not to `elif` or
148
+ # `else`. B2: each branch body is scored one nesting level deeper.
149
+ penalty = 0 if is_elif_arm else increment_by
150
+ out.append(
151
+ Contribution(
152
+ node.lineno,
153
+ describe_node(node, is_elif_arm=is_elif_arm),
154
+ 1 + penalty,
155
+ increment_by,
156
+ not is_elif_arm,
157
+ )
158
+ )
159
+ body_level = increment_by + 1
160
+ _collect_breakdown(node.test, increment_by, node.lineno, out, fold_nested, rec_name, rec_found)
161
+ for stmt in node.body:
162
+ _collect_breakdown(stmt, body_level, node.lineno, out, fold_nested, rec_name, rec_found)
163
+
164
+ orelse = node.orelse
165
+ if len(orelse) == 1 and isinstance(orelse[0], ast.If):
166
+ # `elif`: a sibling at the same nesting level, not a nested `if`.
167
+ _collect_if_breakdown(
168
+ orelse[0], increment_by, out, fold_nested, rec_name, rec_found, is_elif_arm=True
169
+ )
170
+ elif orelse:
171
+ # `else`: +1, no nesting penalty; its body is scored one level deeper.
172
+ out.append(Contribution(orelse[0].lineno, "else", 1, increment_by, False))
173
+ for stmt in orelse:
174
+ _collect_breakdown(stmt, body_level, node.lineno, out, fold_nested, rec_name, rec_found)