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,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