Repository navigation
Expand file tree
/
Copy pathcyclomatic_complexity_bytecode.py
More file actions
202 lines (158 loc) · 5.9 KB
/
Copy pathcyclomatic_complexity_bytecode.py
File metadata and controls
202 lines (158 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""Print cyclomatic complexity per nested Python code object using bytecode.
This is a bytecode-based comparison tool next to the IR-based
`cyclomatic_complexity.py` tool. It emits a recursive JSON tree with the shape:
{
"name": str,
"cyclomatic_complexity": int,
"child_regions": [...]
}
"""
import argparse
import dis
import json
import sys
import types
from typing import Dict, List, Optional, Sequence, Set, Tuple
UNCONDITIONAL_JUMPS = {
"JUMP",
"JUMP_BACKWARD",
"JUMP_BACKWARD_NO_INTERRUPT",
"JUMP_FORWARD",
"JUMP_NO_INTERRUPT",
}
CONDITIONAL_JUMPS = {
"FOR_ITER",
"POP_JUMP_IF_FALSE",
"POP_JUMP_IF_NONE",
"POP_JUMP_IF_NOT_NONE",
"POP_JUMP_IF_TRUE",
"SEND",
}
TERMINATORS = {
"RAISE_VARARGS",
"RERAISE",
"RETURN_CONST",
"RETURN_VALUE",
}
class CodeNode:
def __init__(self, name: str, cyclomatic_complexity: int, child_regions: List["CodeNode"]) -> None:
self.name = name
self.cyclomatic_complexity = cyclomatic_complexity
self.child_regions = child_regions
def to_json(self) -> Dict[str, object]:
return {
"name": self.name,
"cyclomatic_complexity": self.cyclomatic_complexity,
"child_regions": [child.to_json() for child in self.child_regions],
}
def instruction_successors(instructions: Sequence[dis.Instruction], index: int) -> List[int]:
instr = instructions[index]
next_offset = instructions[index + 1].offset if index + 1 < len(instructions) else None
if instr.opname in UNCONDITIONAL_JUMPS:
return [instr.argval]
if instr.opname in CONDITIONAL_JUMPS:
successors = []
if next_offset is not None:
successors.append(next_offset)
successors.append(instr.argval)
return successors
if instr.opname in TERMINATORS:
return []
if next_offset is None:
return []
return [next_offset]
def block_ranges(instructions: Sequence[dis.Instruction], leaders: Set[int]) -> Tuple[List[List[dis.Instruction]], Dict[int, int]]:
blocks: List[List[dis.Instruction]] = []
block_by_offset: Dict[int, int] = {}
current: List[dis.Instruction] = []
for instr in instructions:
if current and instr.offset in leaders:
blocks.append(current)
current = []
current.append(instr)
block_by_offset[instr.offset] = len(blocks)
if current:
blocks.append(current)
return blocks, block_by_offset
def code_complexity(code: types.CodeType) -> int:
instructions = list(dis.get_instructions(code))
if not instructions:
return 1
leaders = {instructions[0].offset}
for index, instr in enumerate(instructions):
successors = instruction_successors(instructions, index)
for target in successors:
leaders.add(target)
next_offset = instructions[index + 1].offset if index + 1 < len(instructions) else None
if next_offset is not None and (len(successors) != 1 or successors[0] != next_offset):
leaders.add(next_offset)
exception_entries = list(dis.Bytecode(code).exception_entries)
instruction_offsets = {instr.offset for instr in instructions}
for entry in exception_entries:
if entry.start in instruction_offsets:
leaders.add(entry.start)
if entry.end in instruction_offsets:
leaders.add(entry.end)
if entry.target in instruction_offsets:
leaders.add(entry.target)
blocks, block_by_offset = block_ranges(instructions, leaders)
if not blocks:
return 1
edges: Set[Tuple[int, int]] = set()
for block_index, block in enumerate(blocks):
last_index = next(i for i, instr in enumerate(instructions) if instr.offset == block[-1].offset)
for target in instruction_successors(instructions, last_index):
if target in block_by_offset:
edges.add((block_index, block_by_offset[target]))
for entry in exception_entries:
if entry.target not in block_by_offset:
continue
target_block = block_by_offset[entry.target]
for block_index, block in enumerate(blocks):
covered = False
for instr in block:
if entry.start <= instr.offset < entry.end:
covered = True
break
if covered:
edges.add((block_index, target_block))
adjacency: Dict[int, Set[int]] = {index: set() for index in range(len(blocks))}
for src, dst in edges:
adjacency[src].add(dst)
adjacency[dst].add(src)
components = 0
seen = set()
for block_index in range(len(blocks)):
if block_index in seen:
continue
components += 1
stack = [block_index]
seen.add(block_index)
while stack:
current = stack.pop()
for neighbor in adjacency[current]:
if neighbor in seen:
continue
seen.add(neighbor)
stack.append(neighbor)
return len(edges) - len(blocks) + 2 * components
def child_code_objects(code: types.CodeType) -> List[types.CodeType]:
return [const for const in code.co_consts if isinstance(const, types.CodeType)]
def build_tree(code: types.CodeType) -> CodeNode:
return CodeNode(
name=code.co_name,
cyclomatic_complexity=code_complexity(code),
child_regions=[build_tree(child) for child in child_code_objects(code)],
)
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("python_file")
args = parser.parse_args(argv)
with open(args.python_file, "r") as f:
source = f.read()
code = compile(source, args.python_file, "exec")
json.dump(build_tree(code).to_json(), sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())