codeanalyzer-python 1.0.2__py3-none-any.whl → 1.1.0__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,331 @@
1
+ """
2
+ Control flow graph for Python programs.
3
+ """
4
+
5
+ import ast
6
+ import sys
7
+ import re
8
+ import token
9
+ import tokenize
10
+ import astor
11
+
12
+ try: # PATCH (codeanalyzer): graphviz is only used by build_visual(), which
13
+ import graphviz as gv # codeanalyzer never calls. Keep the module importable
14
+ except ImportError: # without the graphviz dependency.
15
+ gv = None
16
+
17
+ __all__ = ["Block", "Link", "CFG"]
18
+
19
+ class Block(object):
20
+ """
21
+ Basic block in a control flow graph.
22
+
23
+ Contains a list of statements executed in a program without any control
24
+ jumps. A block of statements is exited through one of its exits. Exits are
25
+ a list of Links that represent control flow jumps.
26
+ """
27
+
28
+ __slots__ = ["id", "statements", "func_calls", "predecessors", "exits"]
29
+
30
+ def __init__(self, id):
31
+ # Id of the block.
32
+ self.id = id
33
+ # Statements in the block.
34
+ self.statements = []
35
+ # Calls to functions inside the block (represents context switches to
36
+ # some functions' CFGs).
37
+ self.func_calls = []
38
+ # Links to predecessors in a control flow graph.
39
+ self.predecessors = []
40
+ # Links to the next blocks in a control flow graph.
41
+ self.exits = []
42
+
43
+ def __del__(self):
44
+ self.statements.clear()
45
+ self.func_calls.clear()
46
+ self.predecessors.clear()
47
+ self.exits.clear()
48
+
49
+ def __str__(self):
50
+ if self.statements:
51
+ return "block:{}@{}".format(self.id, self.at())
52
+ return "empty block:{}".format(self.id)
53
+
54
+ def __repr__(self):
55
+ txt = "{} with {} exits".format(str(self), len(self.exits))
56
+ if self.statements:
57
+ txt += ", body=["
58
+ txt += ", ".join([ast.dump(node) for node in self.statements])
59
+ txt += "]"
60
+ return txt
61
+
62
+ def at(self):
63
+ """
64
+ Get the line number of the first statement of the block in the program.
65
+ """
66
+ if self.statements and self.statements[0].lineno >= 0:
67
+ return self.statements[0].lineno
68
+ return None
69
+
70
+ def is_empty(self):
71
+ """
72
+ Check if the block is empty.
73
+ Returns:
74
+ A boolean indicating if the block is empty (True) or not (False).
75
+ """
76
+ return len(self.statements) == 0
77
+ '''
78
+ def strip_comment(self, src):
79
+ clean_src = ""
80
+
81
+ prev_toktype = token.INDENT
82
+ first_line = None
83
+ last_lineno = -1
84
+ last_col = 0
85
+
86
+ tokgen = tokenize.generate_tokens(src)
87
+ for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen:
88
+ if 0: # Change to if 1 to see the tokens fly by.
89
+ print("%10s %-14s %-20r %r" % (
90
+ tokenize.tok_name.get(toktype, toktype),
91
+ "%d.%d-%d.%d" % (slineno, scol, elineno, ecol),
92
+ ttext, ltext
93
+ ))
94
+ if slineno > last_lineno:
95
+ last_col = 0
96
+ if scol > last_col:
97
+ mod.write(" " * (scol - last_col))
98
+ if toktype == token.STRING and prev_toktype == token.INDENT:
99
+ # Docstring
100
+ mod.write("#--")
101
+ elif toktype == tokenize.COMMENT:
102
+ # Comment
103
+ mod.write("##\n")
104
+ else:
105
+ mod.write(ttext)
106
+ prev_toktype = toktype
107
+ last_col = ecol
108
+ last_lineno = elineno
109
+ '''
110
+ def get_source(self):
111
+ """
112
+ Get a string containing the Python source code corresponding to the
113
+ statements in the block.
114
+ Returns:
115
+ A string containing the source code of the statements.
116
+ """
117
+ src = "#" + str(self.id)+'\n'
118
+ for statement in self.statements:
119
+ if type(statement) in [ast.If, ast.For, ast.While, ast.With]:
120
+ src += (astor.to_source(statement)).split('\n')[0] + "\n"
121
+ elif type(statement) == ast.Try:
122
+ src += (astor.to_source(statement)).split('\n')[0] + "\n"
123
+ #elif type(statement) == ast.If:
124
+ # src += (astor.to_source(statement)).split('\n')[0] + "\n"
125
+ elif type(statement) in [ast.FunctionDef,ast.AsyncFunctionDef,
126
+ ast.ClassDef]:
127
+ src += (astor.to_source(statement)).split('\n')[0] + "...\n"
128
+ elif type(statement) == ast.ClassDef:
129
+ src += (astor.to_source(statement)).split('\n')[0] + "...\n"
130
+ else:
131
+ src += astor.to_source(statement)
132
+ return src
133
+
134
+ def get_calls(self):
135
+ """
136
+ Get a string containing the calls to other functions inside the block.
137
+
138
+ Returns:
139
+ A string containing the names of the functions called inside the
140
+ block.
141
+ """
142
+ txt = ""
143
+ for func_call_entry in self.func_calls:
144
+ txt += func_call_entry['name'] + '\n'
145
+ return txt
146
+
147
+
148
+ class Link(object):
149
+ """
150
+ Link between blocks in a control flow graph.
151
+
152
+ Represents a control flow jump between two blocks. Contains an exitcase in
153
+ the form of an expression, representing the case in which the associated
154
+ control jump is made.
155
+ """
156
+
157
+ __slots__ = ["source", "target", "exitcase"]
158
+
159
+ def __init__(self, source, target, exitcase=None):
160
+ assert type(source) == Block, "Source of a link must be a block"
161
+ assert type(target) == Block, "Target of a link must be a block"
162
+ # Block from which the control flow jump was made.
163
+ self.source = source
164
+ # Target block of the control flow jump.
165
+ self.target = target
166
+ # 'Case' leading to a control flow jump through this link.
167
+ self.exitcase = exitcase
168
+
169
+ def __str__(self):
170
+ return "link from {} to {}".format(str(self.source), str(self.target))
171
+
172
+ def __repr__(self):
173
+ if self.exitcase is not None:
174
+ return "{}, with exitcase {}".format(str(self),
175
+ ast.dump(self.exitcase))
176
+ return str(self)
177
+
178
+ def get_exitcase(self):
179
+ """
180
+ Get a string containing the Python source code corresponding to the
181
+ exitcase of the Link.
182
+
183
+ Returns:
184
+ A string containing the source code.
185
+ """
186
+ if self.exitcase:
187
+ return astor.to_source(self.exitcase)
188
+ return ""
189
+ def __del__(self):
190
+ self.source = None
191
+ # Target block of the control flow jump.
192
+ self.target = None
193
+ # 'Case' leading to a control flow jump through this link.
194
+ self.exitcase = None
195
+
196
+
197
+
198
+ class CFG(object):
199
+ """
200
+ Control flow graph (CFG).
201
+
202
+ A control flow graph is composed of basic blocks and links between them
203
+ representing control flow jumps. It has a unique entry block and several
204
+ possible 'final' blocks (blocks with no exits representing the end of the
205
+ CFG).
206
+ """
207
+ def __init__(self, name, asynchr=False):
208
+ """
209
+ The constructor of CFG class. Only name of this graph is required.
210
+ """
211
+ assert type(name) == str, "Name of a CFG must be a string"
212
+ assert type(asynchr) == bool, "Async must be a boolean value"
213
+ # Name of the function or module being represented.
214
+ self.name = name
215
+ # Type of function represented by the CFG (sync or async). A Python
216
+ # program is considered as a synchronous function (main).
217
+ self.asynchr = asynchr
218
+ # Entry block of the CFG.
219
+ self.entryblock = None
220
+ # Final blocks of the CFG.
221
+ self.finalblocks = []
222
+ # Sub-CFGs for functions defined inside the current CFG.
223
+ self.functioncfgs = {}
224
+ self.class_cfgs = {}
225
+
226
+ self.function_args = {}
227
+ self.class_args = {}
228
+
229
+ def __del__(self):
230
+ pass
231
+
232
+ def __str__(self):
233
+ return "CFG for {}".format(self.name)
234
+
235
+ def remove_comments(self, src):
236
+ pass
237
+
238
+
239
+
240
+ def get_all_blocks(self):
241
+ """
242
+ Get a list of code blocks in this CFG; This is generated by BFS order.
243
+
244
+ Returns:
245
+ A list of code blocks.
246
+ """
247
+ import queue
248
+ all_blocks = []
249
+
250
+ visited = set()
251
+ working_queue = queue.Queue()
252
+ working_queue.put(self.entryblock)
253
+
254
+ while not working_queue.empty():
255
+ block = working_queue.get()
256
+ # this block has been visited
257
+ if block.id in visited:
258
+ continue
259
+ all_blocks.append(block)
260
+ visited.add(block.id)
261
+ for suc_link in block.exits:
262
+ if suc_link.target.id not in visited:
263
+ working_queue.put(suc_link.target)
264
+ return all_blocks
265
+ #def dfs(start_block):
266
+ # # non-recurisve implementation of DFS search
267
+
268
+ def __iter__(self):
269
+ """
270
+ Generator that yields all the blocks in the current graph, then
271
+ recursively yields from any sub graphs
272
+ """
273
+ visited = set()
274
+ to_visit = [self.entryblock]
275
+
276
+ while to_visit:
277
+ block = to_visit.pop(0)
278
+ visited.add(block)
279
+ for exit_ in block.exits:
280
+ if exit_.target in visited or exit_.target in to_visit:
281
+ continue
282
+ to_visit.append(exit_.target)
283
+ yield block
284
+
285
+ for subcfg in self.functioncfgs.values():
286
+ yield from subcfg
287
+
288
+ def _visit_blocks(self, graph, block, visited=[], calls=True):
289
+ # Don't visit blocks twice.
290
+ if block.id in visited:
291
+ return
292
+
293
+ nodelabel = block.get_source()
294
+ graph.node(str(block.id), label=nodelabel)
295
+
296
+ visited.append(block.id)
297
+ # Show the block's function calls in a node.
298
+ if calls and block.func_calls:
299
+ calls_node = str(block.id)+"_calls"
300
+ calls_label = block.get_calls().strip()
301
+ graph.node(calls_node, label=calls_label,
302
+ _attributes={'shape': 'box'})
303
+ graph.edge(str(block.id), calls_node, label="calls",
304
+ _attributes={'style': 'dashed'})
305
+ # Recursively visit all the blocks of the CFG.
306
+ for exit in block.exits:
307
+ self._visit_blocks(graph, exit.target, visited, calls=calls)
308
+ edgelabel = exit.get_exitcase().strip()
309
+ graph.edge(str(block.id), str(exit.target.id), label=edgelabel)
310
+
311
+ def _build_visual(self, format='pdf', calls=True):
312
+ graph = gv.Digraph(name='cluster'+self.name, format=format,
313
+ graph_attr={'label': self.name})
314
+ self._visit_blocks(graph, self.entryblock, visited=[], calls=False)
315
+ return graph
316
+
317
+ def build_visual(self, format, calls=True, show=True):
318
+ """
319
+ Build a visualisation of the CFG with graphviz and output it in a DOT
320
+ file.
321
+
322
+ Args:
323
+ filename: The name of the output file in which the visualisation
324
+ must be saved.
325
+ format: The format to use for the output file (PDF, ...).
326
+ show: A boolean indicating whether to automatically open the output
327
+ file after building the visualisation.
328
+ """
329
+ graph = self._build_visual(format, calls)
330
+ return graph
331
+
File without changes
@@ -0,0 +1,234 @@
1
+ import sys
2
+ import ast
3
+ from collections import deque
4
+ from ast import NodeVisitor
5
+ from copy import deepcopy
6
+
7
+
8
+ def is_py38_or_higher():
9
+ if sys.version_info.major == 3 and sys.version_info.minor >= 8:
10
+ return True
11
+ return False
12
+
13
+
14
+ NAMECONSTANT_TYPE = ast.Constant if is_py38_or_higher() else ast.NameConstant
15
+
16
+
17
+ class CallTransformer(ast.NodeTransformer):
18
+ def __init__(self):
19
+ self.call_names = []
20
+
21
+ def visit_Attribute(self, node):
22
+ # self.generic_visit(node.value)
23
+ return node
24
+
25
+ def param2str(self, param):
26
+
27
+ def get_func(node):
28
+ if type(node) == ast.Name:
29
+ return node.id
30
+ elif type(node) == ast.Constant:
31
+ # ingore such as "this is a constant".join()
32
+ return ""
33
+ elif type(node) == ast.BinOp:
34
+ # ingore such as (a+b+c).fun()
35
+ return ""
36
+ elif type(node) == ast.Str:
37
+ # ingore such as "xxx".fun()
38
+ return ""
39
+ elif type(node) == ast.JoinedStr:
40
+ # ingore such as "xxx".fun()
41
+ return ""
42
+ elif type(node) == ast.Bytes:
43
+ # ingore such as "xxx".fun()
44
+ return ""
45
+ elif type(node) == ast.Compare:
46
+ # example "(x.matrix_exp() == torch.eye(20, 20, dtype=dtype, device=device)).all().item()"
47
+ # tests/test-cases/cfg-tests/pytorch-test-test_linalg.py
48
+ # ignore for now
49
+ return ""
50
+ elif type(node) == ast.Subscript:
51
+ # currently, we will ignore the slices because we cannot track the type of the value.
52
+ # for instance, a[something].fun() -> a.fun()
53
+ # this sacrifice
54
+ return get_func(node.value)
55
+ #elif type(node) == ast.JoinedStr:
56
+ # return ""
57
+ elif type(node) == ast.Attribute:
58
+ if type(node.value) in [ast.JoinedStr, ast.Constant]:
59
+ return node.attr
60
+ else:
61
+ return get_func(node.value) + "." + node.attr
62
+ elif type(node) == ast.Call:
63
+ return get_func(node.func)
64
+ elif type(node) == ast.IfExp:
65
+ return ""
66
+ elif type(node) == ast.Compare:
67
+ return ""
68
+ elif type(node) == ast.UnaryOp:
69
+ return ""
70
+ #ast.UnaryOp
71
+ else:
72
+ #import astor
73
+ #print(astor.to_source(node))
74
+ raise Exception(str(type(node)))
75
+
76
+ if isinstance(param, ast.Subscript):
77
+ return self.param2str(param.value)
78
+ if isinstance(param, ast.Call):
79
+ return get_func(param)
80
+ elif isinstance(param, ast.Name):
81
+ return param.id
82
+ elif isinstance(param, ast.Num):
83
+ # python 3.6
84
+ return param.n
85
+ #return param.value
86
+ elif isinstance(param, ast.List):
87
+ return "List"
88
+ elif isinstance(param, ast.ListComp):
89
+ return "List"
90
+ elif isinstance(param, ast.Tuple):
91
+ return "Tuple"
92
+ elif isinstance(param, (ast.Dict, ast.DictComp)):
93
+ return "Dict"
94
+ elif isinstance(param, (ast.Set, ast.SetComp)):
95
+ return "Set"
96
+ elif isinstance(param, ast.Str):
97
+ return param.s
98
+ elif isinstance(param, ast.NameConstant):
99
+ return param.value
100
+ elif isinstance(param, ast.Constant):
101
+ return param.value
102
+ elif isinstance(param, ast.Expr):
103
+ return "Expr"
104
+ else:
105
+ return "unknown"
106
+
107
+ def visit_Call(self, node):
108
+
109
+ tmp_fun_node = deepcopy(node)
110
+ tmp_fun_node.args = []
111
+ tmp_fun_node.keywords = []
112
+
113
+ callvisitor = FuncCallVisitor()
114
+ callvisitor.visit(tmp_fun_node)
115
+
116
+ call_info = {"name": callvisitor.name,
117
+ "lineno": tmp_fun_node.lineno,
118
+ "col_offset": tmp_fun_node.col_offset,
119
+ "params": []
120
+ }
121
+ self.call_names += [call_info]
122
+ for arg in node.args:
123
+ call_info["params"] += [self.param2str(arg)]
124
+ self.generic_visit(arg)
125
+
126
+ for kw in node.keywords:
127
+ call_info["params"] += [self.param2str(kw.value)]
128
+ self.generic_visit(kw)
129
+ self.generic_visit(tmp_fun_node)
130
+
131
+ return node
132
+
133
+
134
+ class FuncCallVisitor(ast.NodeVisitor):
135
+ def __init__(self):
136
+ self._name = deque()
137
+ self.call_names = []
138
+
139
+ def clear(self):
140
+ self._name = deque()
141
+ self.call_names = []
142
+
143
+ @property
144
+ def name(self):
145
+ return '.'.join(self._name)
146
+
147
+ @name.deleter
148
+ def name(self):
149
+ self._name.clear()
150
+
151
+ def visit_Name(self, node):
152
+ self._name.appendleft(node.id)
153
+
154
+ def visit_Attribute(self, node):
155
+
156
+ try:
157
+ self._name.appendleft(node.attr)
158
+ self._name.appendleft(node.value.id)
159
+ except AttributeError as e:
160
+ self.generic_visit(node)
161
+
162
+ def visit_Call(self, node):
163
+ node.args = []
164
+ node.keywords = []
165
+ self.generic_visit(node)
166
+ return node
167
+
168
+
169
+ def visit_Subscript(self, node):
170
+ # ingore subscription slice
171
+ self.visit(node.value)
172
+ return node
173
+
174
+ def get_args(node):
175
+ arg_type = []
176
+ for arg in node.args:
177
+ if isinstance(arg, ast.Name):
178
+ arg_type.append(arg.id)
179
+ elif isinstance(arg, ast.Num):
180
+ arg_type.append("Num")
181
+ elif isinstance(arg, ast.List):
182
+ arg_type.append("List")
183
+ elif isinstance(arg, ast.ListComp):
184
+ arg_type.append("List")
185
+ elif isinstance(arg, ast.Tuple):
186
+ arg_type.append("Tuple")
187
+ elif isinstance(arg, ast.Dict):
188
+ arg_type.append("Dict")
189
+ elif isinstance(arg, ast.DictComp):
190
+ arg_type.append("Dict")
191
+ elif isinstance(arg, ast.Set):
192
+ arg_type.append("Set")
193
+ elif isinstance(arg, ast.SetComp):
194
+ arg_type.append("Set")
195
+ elif isinstance(arg, ast.Str):
196
+ arg_type.append("Str")
197
+ elif isinstance(arg, ast.NameConstant):
198
+ arg_type.append("NameConstant")
199
+ elif isinstance(arg, ast.Constant):
200
+ arg_type.append("Constant")
201
+ elif isinstance(arg, ast.Call):
202
+ arg_type.append(("Call", get_func_calls(arg)[0]))
203
+ else:
204
+ arg_type.append("Other")
205
+ return arg_type
206
+
207
+
208
+ def get_call_type(tree):
209
+ # how to remove
210
+ func_calls = []
211
+ for node in ast.walk(tree):
212
+ if isinstance(node, ast.Call):
213
+ callvisitor = FuncCallVisitor()
214
+ callvisitor.visit(node.func)
215
+ func_calls += [(callvisitor.name, get_args(node))]
216
+ return func_calls
217
+
218
+
219
+ def get_call_type(tree):
220
+ # how to remove
221
+ func_calls = []
222
+ for node in ast.walk(tree):
223
+ if isinstance(node, ast.Call):
224
+ callvisitor = FuncCallVisitor()
225
+ callvisitor.visit(node.func)
226
+ func_calls += [(callvisitor.name, get_args(node))]
227
+ return func_calls
228
+
229
+
230
+ def get_func_calls(tree):
231
+ node = deepcopy(tree)
232
+ transformer = CallTransformer()
233
+ transformer.visit(node)
234
+ return transformer.call_names