]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - misc/coding_tools/check-miscaptures.py
c1ced6c3f1369675cdb16015d9f74d59bdab1936
[tahoe-lafs/tahoe-lafs.git] / misc / coding_tools / check-miscaptures.py
1 #! /usr/bin/python
2
3 import os, sys, compiler
4 from compiler.ast import Node, For, While, ListComp, AssName, Name, Lambda, Function
5
6
7 def check_source(source):
8     return check_thing(compiler.parse, source)
9
10 def check_file(path):
11     return check_thing(compiler.parseFile, path)
12
13 def check_thing(parser, thing):
14     try:
15         ast = parser(thing)
16     except SyntaxError, e:
17         return [e]
18     else:
19         results = []
20         check_ast(ast, results)
21         return results
22
23 def check_ast(ast, results):
24     """Check a node outside a loop."""
25     if isinstance(ast, (For, While, ListComp)):
26         check_loop(ast, results)
27     else:
28         for child in ast.getChildNodes():
29             if isinstance(ast, Node):
30                 check_ast(child, results)
31
32 def check_loop(ast, results):
33     """Check a particular outer loop."""
34
35     # List comprehensions have a poorly designed AST of the form
36     # ListComp(exprNode, [ListCompFor(...), ...]), in which the
37     # result expression is outside the ListCompFor node even though
38     # it is logically inside the loop(s).
39     # There may be multiple ListCompFor nodes (in cases such as
40     #   [lambda: (a,b) for a in ... for b in ...]
41     # ), and that case they are not nested in the AST. But these
42     # warts (nonobviously) happen not to matter for our analysis.
43
44     declared = {}  # maps name to lineno of declaration
45     nested = set()
46     collect_declared_and_nested(ast, declared, nested)
47
48     # For each nested function...
49     for funcnode in nested:
50         # Check for captured variables in this function.
51         captured = set()
52         collect_captured(funcnode, declared, captured)
53         for name in captured:
54             # We want to report the outermost capturing function
55             # (since that is where the workaround will need to be
56             # added), and the variable declaration. Just one report
57             # per capturing function per variable will do.
58             results.append(make_result(funcnode, name, declared[name]))
59
60         # Check each node in the function body in case it
61         # contains another 'for' loop.
62         childnodes = funcnode.getChildNodes()[len(funcnode.defaults):]
63         for child in childnodes:
64             check_ast(funcnode, results)
65
66 def collect_declared_and_nested(ast, declared, nested):
67     """
68     Collect the names declared in this 'for' loop, not including
69     names declared in nested functions. Also collect the nodes of
70     functions that are nested one level deep.
71     """
72     if isinstance(ast, AssName):
73         declared[ast.name] = ast.lineno
74     else:
75         childnodes = ast.getChildNodes()
76         if isinstance(ast, (Lambda, Function)):
77             nested.add(ast)
78
79             # The default argument expressions are "outside" the
80             # function, even though they are children of the
81             # Lambda or Function node.
82             childnodes = childnodes[:len(ast.defaults)]
83
84         for child in childnodes:
85             if isinstance(ast, Node):
86                 collect_declared_and_nested(child, declared, nested)
87
88 def collect_captured(ast, declared, captured):
89     """Collect any captured variables that are also in declared."""
90     if isinstance(ast, Name):
91         if ast.name in declared:
92             captured.add(ast.name)
93     else:
94         childnodes = ast.getChildNodes()
95
96         if isinstance(ast, (Lambda, Function)):
97             # Formal parameters of the function are excluded from
98             # captures we care about in subnodes of the function body.
99             new_declared = declared.copy()
100             remove_argnames(ast.argnames, new_declared)
101
102             for child in childnodes[len(ast.defaults):]:
103                 collect_captured(child, declared, captured)
104
105             # The default argument expressions are "outside" the
106             # function, even though they are children of the
107             # Lambda or Function node.
108             childnodes = childnodes[:len(ast.defaults)]
109
110         for child in childnodes:
111             if isinstance(ast, Node):
112                 collect_captured(child, declared, captured)
113
114
115 def remove_argnames(names, fromset):
116     for element in names:
117         if element in fromset:
118             del fromset[element]
119         elif isinstance(element, (tuple, list)):
120             remove_argnames(element, fromset)
121
122
123 def make_result(funcnode, var_name, var_lineno):
124     if hasattr(funcnode, 'name'):
125         func_name = 'function %r' % (funcnode.name,)
126     else:
127         func_name = '<lambda>'
128     return (funcnode.lineno, func_name, var_name, var_lineno)
129
130 def report(out, path, results):
131     for r in results:
132         if isinstance(r, SyntaxError):
133             print >>out, path + (" NOT ANALYSED due to syntax error: %s" % r)
134         else:
135             print >>out, path + (":%r %s captures %r declared at line %d" % r)
136
137 def check(sources, out):
138     class Counts:
139         n = 0
140         processed_files = 0
141         suspect_files = 0
142     counts = Counts()
143
144     def _process(path):
145         results = check_file(path)
146         report(out, path, results)
147         counts.n += len(results)
148         counts.processed_files += 1
149         if len(results) > 0:
150             counts.suspect_files += 1
151
152     for source in sources:
153         print >>out, "Checking %s..." % (source,)
154         if os.path.isfile(source):
155             _process(source)
156         else:
157             for (dirpath, dirnames, filenames) in os.walk(source):
158                 for fn in filenames:
159                     (basename, ext) = os.path.splitext(fn)
160                     if ext == '.py':
161                         _process(os.path.join(dirpath, fn))
162
163     print >>out, ("%d suspiciously captured variables in %d out of %d files"
164                   % (counts.n, counts.suspect_files, counts.processed_files))
165     return counts.n
166
167
168 sources = ['src']
169 if len(sys.argv) > 1:
170     sources = sys.argv[1:]
171 if check(sources, sys.stderr) > 0:
172     sys.exit(1)
173
174
175 # TODO: self-tests