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.
- codecoco-3.5.1.dist-info/METADATA +278 -0
- codecoco-3.5.1.dist-info/RECORD +16 -0
- codecoco-3.5.1.dist-info/WHEEL +5 -0
- codecoco-3.5.1.dist-info/entry_points.txt +2 -0
- codecoco-3.5.1.dist-info/licenses/LICENSE +22 -0
- codecoco-3.5.1.dist-info/top_level.txt +1 -0
- cognitive_complexity/__init__.py +1 -0
- cognitive_complexity/api.py +174 -0
- cognitive_complexity/autofix.py +238 -0
- cognitive_complexity/cli.py +360 -0
- cognitive_complexity/common_types.py +46 -0
- cognitive_complexity/discovery.py +193 -0
- cognitive_complexity/refactor.py +388 -0
- cognitive_complexity/report.py +103 -0
- cognitive_complexity/utils/__init__.py +0 -0
- cognitive_complexity/utils/ast.py +165 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
|
|
4
|
+
from cognitive_complexity.common_types import AnyFuncdef, is_funcdef
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def call_targets_name(call: ast.Call, name: str) -> bool:
|
|
8
|
+
func = call.func
|
|
9
|
+
if isinstance(func, ast.Name):
|
|
10
|
+
return func.id == name
|
|
11
|
+
if isinstance(func, ast.Attribute) and func.attr == name:
|
|
12
|
+
# method recursion: self.name(...) / cls.name(...)
|
|
13
|
+
return isinstance(func.value, ast.Name) and func.value.id in ("self", "cls")
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _returns_name(stmt: ast.stmt, name: str) -> bool:
|
|
18
|
+
return (
|
|
19
|
+
isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Name) and stmt.value.id == name
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def decorator_inner(funcdef: AnyFuncdef) -> AnyFuncdef | None:
|
|
24
|
+
"""The single inner function a decorator/closure factory returns, else ``None``.
|
|
25
|
+
|
|
26
|
+
A function that defines exactly one inner function and returns *that function
|
|
27
|
+
by name* is structurally a decorator (or value-returning closure factory); in
|
|
28
|
+
fold mode it is scored as that inner function (pre-2.0.0 compat). Returning
|
|
29
|
+
the node — rather than a bool — lets callers use the narrowed inner directly,
|
|
30
|
+
so they need not re-index and re-type ``funcdef.body[0]``.
|
|
31
|
+
"""
|
|
32
|
+
if len(funcdef.body) != 2:
|
|
33
|
+
return None
|
|
34
|
+
inner = funcdef.body[0]
|
|
35
|
+
if is_funcdef(inner) and _returns_name(funcdef.body[1], inner.name):
|
|
36
|
+
return inner
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _walk_own_scope(funcdef: AnyFuncdef) -> Iterator[ast.AST]:
|
|
41
|
+
"""Yield every node in ``funcdef``'s own scope, not descending into nested defs.
|
|
42
|
+
|
|
43
|
+
Named nested ``def``/``async def`` are independent scoring units (Option A),
|
|
44
|
+
so a call living inside one belongs to that unit, not to ``funcdef``. Walking
|
|
45
|
+
the whole subtree (``ast.walk``) would miscount a call to ``funcdef``'s name
|
|
46
|
+
made from inside a nested def as the outer function's recursion. The walk is
|
|
47
|
+
iterative (an explicit stack) so a deeply nested expression can't blow the
|
|
48
|
+
interpreter's recursion limit just while we look for recursion.
|
|
49
|
+
"""
|
|
50
|
+
stack: list[ast.AST] = list(ast.iter_child_nodes(funcdef))
|
|
51
|
+
while stack:
|
|
52
|
+
node = stack.pop()
|
|
53
|
+
yield node
|
|
54
|
+
if not is_funcdef(node):
|
|
55
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def has_recursive_calls(funcdef: AnyFuncdef) -> bool:
|
|
59
|
+
# Direct recursion only: a call by the function's own name, or a
|
|
60
|
+
# self/cls method call to it. Indirect/mutual recursion (a -> b -> a) is
|
|
61
|
+
# not detected; that needs a whole-program call graph, out of scope for a
|
|
62
|
+
# per-function AST metric. The walk stays within funcdef's own scope so a
|
|
63
|
+
# call to its name from inside a nested def is not miscounted here.
|
|
64
|
+
return any(
|
|
65
|
+
call_targets_name(node, funcdef.name)
|
|
66
|
+
for node in _walk_own_scope(funcdef)
|
|
67
|
+
if isinstance(node, ast.Call)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Static node-type -> label dispatch. ``ast.If`` is handled separately because
|
|
72
|
+
# its label depends on whether it is an elif arm, which is positional. Named
|
|
73
|
+
# nested ``def``s are not here: they are scored as their own reporting units, not
|
|
74
|
+
# as a construct of the enclosing function (see ``api._collect_breakdown``).
|
|
75
|
+
_NODE_LABELS: tuple[tuple[type[ast.AST] | tuple[type[ast.AST], ...], str], ...] = (
|
|
76
|
+
(ast.IfExp, "ternary"),
|
|
77
|
+
((ast.For, ast.AsyncFor), "for"),
|
|
78
|
+
(ast.While, "while"),
|
|
79
|
+
(ast.ExceptHandler, "except"),
|
|
80
|
+
(ast.Match, "match"),
|
|
81
|
+
(ast.Lambda, "lambda"),
|
|
82
|
+
(ast.BoolOp, "bool-op"),
|
|
83
|
+
(ast.comprehension, "comprehension-if"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def describe_node(node: ast.AST, *, is_elif_arm: bool = False) -> str:
|
|
88
|
+
"""Short human label for a scored construct, used in breakdowns.
|
|
89
|
+
|
|
90
|
+
Whether an ``ast.If`` is an ``elif`` depends on its position (is it the
|
|
91
|
+
``else`` branch of another ``if``?), which the node cannot know on its own,
|
|
92
|
+
so the caller supplies ``is_elif_arm``.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(node, ast.If):
|
|
95
|
+
return "elif" if is_elif_arm else "if"
|
|
96
|
+
for types, label in _NODE_LABELS:
|
|
97
|
+
if isinstance(node, types):
|
|
98
|
+
return label
|
|
99
|
+
return type(node).__name__
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def process_control_flow_breaker(
|
|
103
|
+
node: ast.For | ast.AsyncFor | ast.While | ast.IfExp | ast.ExceptHandler | ast.Match,
|
|
104
|
+
increment_by: int,
|
|
105
|
+
) -> tuple[int, int, bool]:
|
|
106
|
+
# `ast.If` is handled by api._collect_if_breakdown, not here, so that
|
|
107
|
+
# if/elif/else chains can score body and orelse at different nesting levels.
|
|
108
|
+
if isinstance(node, ast.IfExp):
|
|
109
|
+
# C if A else B; ternary operator equivalent
|
|
110
|
+
increment = 0
|
|
111
|
+
increment_by += 1
|
|
112
|
+
elif isinstance(node, ast.ExceptHandler):
|
|
113
|
+
# +1 for the catch/except-handler
|
|
114
|
+
increment = 0
|
|
115
|
+
increment_by += 1
|
|
116
|
+
elif isinstance(node, ast.Match):
|
|
117
|
+
# a match/switch is a single structural increment plus a nesting level,
|
|
118
|
+
# regardless of the number of cases (Sonar treats switch as one branch)
|
|
119
|
+
increment = 0
|
|
120
|
+
increment_by += 1
|
|
121
|
+
elif node.orelse:
|
|
122
|
+
# +1 for the else and add a nesting level
|
|
123
|
+
increment = 1
|
|
124
|
+
increment_by += 1
|
|
125
|
+
else:
|
|
126
|
+
# no 'else' to count, just add a nesting level
|
|
127
|
+
increment = 0
|
|
128
|
+
increment_by += 1
|
|
129
|
+
return increment_by, max(1, increment_by) + increment, True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def process_node_itself(
|
|
133
|
+
node: ast.AST,
|
|
134
|
+
increment_by: int,
|
|
135
|
+
fold_nested: bool = False,
|
|
136
|
+
) -> tuple[int, int, bool]:
|
|
137
|
+
# `ast.If` is intercepted by api._collect_if_breakdown before reaching here.
|
|
138
|
+
control_flow_breakers = (
|
|
139
|
+
ast.For,
|
|
140
|
+
ast.AsyncFor,
|
|
141
|
+
ast.While,
|
|
142
|
+
ast.IfExp,
|
|
143
|
+
ast.ExceptHandler,
|
|
144
|
+
ast.Match,
|
|
145
|
+
)
|
|
146
|
+
# Lambdas always add a nesting level and fold in. In the default unit mode
|
|
147
|
+
# named nested `def`s are scored as their own units and never reach here; in
|
|
148
|
+
# fold mode (pre-2.0.0 compat) they too add a nesting level and fold in.
|
|
149
|
+
incrementers_nodes: tuple[type[ast.AST], ...] = (
|
|
150
|
+
(ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) if fold_nested else (ast.Lambda,)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if isinstance(node, control_flow_breakers):
|
|
154
|
+
return process_control_flow_breaker(node, increment_by)
|
|
155
|
+
elif isinstance(node, incrementers_nodes):
|
|
156
|
+
increment_by += 1
|
|
157
|
+
return increment_by, 0, True
|
|
158
|
+
elif isinstance(node, ast.BoolOp):
|
|
159
|
+
inner_boolops_amount = len([n for n in ast.walk(node) if isinstance(n, ast.BoolOp)])
|
|
160
|
+
base_complexity = inner_boolops_amount
|
|
161
|
+
return increment_by, base_complexity, False
|
|
162
|
+
elif isinstance(node, ast.comprehension):
|
|
163
|
+
# each filter condition in a comprehension is a decision point
|
|
164
|
+
return increment_by, len(node.ifs), True
|
|
165
|
+
return increment_by, 0, True
|