codeparser: fix ShellSyntaxError usage
[bitbake.git] / lib / bb / codeparser.py
1 import ast
2 import codegen
3 import logging
4 import os.path
5 import bb.utils, bb.data
6 from itertools import chain
7 from pysh import pyshyacc, pyshlex, sherrors
8
9
10 logger = logging.getLogger('BitBake.CodeParser')
11 PARSERCACHE_VERSION = 2
12
13 try:
14     import cPickle as pickle
15 except ImportError:
16     import pickle
17     logger.info('Importing cPickle failed.  Falling back to a very slow implementation.')
18
19
20 def check_indent(codestr):
21     """If the code is indented, add a top level piece of code to 'remove' the indentation"""
22
23     i = 0
24     while codestr[i] in ["\n", "        ", " "]:
25         i = i + 1
26
27     if i == 0:
28         return codestr
29
30     if codestr[i-1] is "        " or codestr[i-1] is " ":
31         return "if 1:\n" + codestr
32
33     return codestr
34
35 pythonparsecache = {}
36 shellparsecache = {}
37
38 def parser_cachefile(d):
39     cachedir = (bb.data.getVar("PERSISTENT_DIR", d, True) or
40                 bb.data.getVar("CACHE", d, True))
41     if cachedir in [None, '']:
42         return None
43     bb.utils.mkdirhier(cachedir)
44     cachefile = os.path.join(cachedir, "bb_codeparser.dat")
45     logger.debug(1, "Using cache in '%s' for codeparser cache", cachefile)
46     return cachefile
47
48 def parser_cache_init(d):
49     global pythonparsecache
50     global shellparsecache
51
52     cachefile = parser_cachefile(d)
53     if not cachefile:
54         return
55
56     try:
57         p = pickle.Unpickler(file(cachefile, "rb"))
58         data, version = p.load()
59     except:
60         return
61
62     if version != PARSERCACHE_VERSION:
63         return
64
65     pythonparsecache = data[0]
66     shellparsecache = data[1]
67
68 def parser_cache_save(d):
69     cachefile = parser_cachefile(d)
70     if not cachefile:
71         return
72
73     p = pickle.Pickler(file(cachefile, "wb"), -1)
74     p.dump([[pythonparsecache, shellparsecache], PARSERCACHE_VERSION])
75
76 class PythonParser():
77     class ValueVisitor():
78         """Visitor to traverse a python abstract syntax tree and obtain
79         the variables referenced via bitbake metadata APIs, and the external
80         functions called.
81         """
82
83         getvars = ("d.getVar", "bb.data.getVar", "data.getVar")
84         expands = ("d.expand", "bb.data.expand", "data.expand")
85         execs = ("bb.build.exec_func", "bb.build.exec_task")
86
87         @classmethod
88         def _compare_name(cls, strparts, node):
89             """Given a sequence of strings representing a python name,
90             where the last component is the actual Name and the prior
91             elements are Attribute nodes, determine if the supplied node
92             matches.
93             """
94
95             if not strparts:
96                 return True
97
98             current, rest = strparts[0], strparts[1:]
99             if isinstance(node, ast.Attribute):
100                 if current == node.attr:
101                     return cls._compare_name(rest, node.value)
102             elif isinstance(node, ast.Name):
103                 if current == node.id:
104                     return True
105             return False
106
107         @classmethod
108         def compare_name(cls, value, node):
109             """Convenience function for the _compare_node method, which
110             can accept a string (which is split by '.' for you), or an
111             iterable of strings, in which case it checks to see if any of
112             them match, similar to isinstance.
113             """
114
115             if isinstance(value, basestring):
116                 return cls._compare_name(tuple(reversed(value.split("."))),
117                                          node)
118             else:
119                 return any(cls.compare_name(item, node) for item in value)
120
121         def __init__(self, value):
122             self.var_references = set()
123             self.var_execs = set()
124             self.direct_func_calls = set()
125             self.var_expands = set()
126             self.value = value
127
128         @classmethod
129         def warn(cls, func, arg):
130             """Warn about calls of bitbake APIs which pass a non-literal
131             argument for the variable name, as we're not able to track such
132             a reference.
133             """
134
135             try:
136                 funcstr = codegen.to_source(func)
137                 argstr = codegen.to_source(arg)
138             except TypeError:
139                 logger.debug(2, 'Failed to convert function and argument to source form')
140             else:
141                 logger.debug(1, "Warning: in call to '%s', argument '%s' is "
142                                 "not a literal", funcstr, argstr)
143
144         def visit_Call(self, node):
145             if self.compare_name(self.getvars, node.func):
146                 if isinstance(node.args[0], ast.Str):
147                     self.var_references.add(node.args[0].s)
148                 else:
149                     self.warn(node.func, node.args[0])
150             elif self.compare_name(self.expands, node.func):
151                 if isinstance(node.args[0], ast.Str):
152                     self.warn(node.func, node.args[0])
153                     self.var_expands.update(node.args[0].s)
154                 elif isinstance(node.args[0], ast.Call) and \
155                      self.compare_name(self.getvars, node.args[0].func):
156                     pass
157                 else:
158                     self.warn(node.func, node.args[0])
159             elif self.compare_name(self.execs, node.func):
160                 if isinstance(node.args[0], ast.Str):
161                     self.var_execs.add(node.args[0].s)
162                 else:
163                     self.warn(node.func, node.args[0])
164             elif isinstance(node.func, ast.Name):
165                 self.direct_func_calls.add(node.func.id)
166             elif isinstance(node.func, ast.Attribute):
167                 # We must have a qualified name.  Therefore we need
168                 # to walk the chain of 'Attribute' nodes to determine
169                 # the qualification.
170                 attr_node = node.func.value
171                 identifier = node.func.attr
172                 while isinstance(attr_node, ast.Attribute):
173                     identifier = attr_node.attr + "." + identifier
174                     attr_node = attr_node.value
175                 if isinstance(attr_node, ast.Name):
176                     identifier = attr_node.id + "." + identifier
177                 self.direct_func_calls.add(identifier)
178
179     def __init__(self):
180         #self.funcdefs = set()
181         self.execs = set()
182         #self.external_cmds = set()
183         self.references = set()
184
185     def parse_python(self, node):
186
187         h = hash(str(node))
188
189         if h in pythonparsecache:
190             self.references = pythonparsecache[h]["refs"]
191             self.execs = pythonparsecache[h]["execs"]
192             return
193
194         code = compile(check_indent(str(node)), "<string>", "exec",
195                        ast.PyCF_ONLY_AST)
196
197         visitor = self.ValueVisitor(code)
198         for n in ast.walk(code):
199             if n.__class__.__name__ == "Call":
200                 visitor.visit_Call(n)
201
202         self.references.update(visitor.var_references)
203         self.references.update(visitor.var_execs)
204         self.execs = visitor.direct_func_calls
205
206         pythonparsecache[h] = {}
207         pythonparsecache[h]["refs"] = self.references
208         pythonparsecache[h]["execs"] = self.execs
209
210 class ShellParser():
211     def __init__(self):
212         self.funcdefs = set()
213         self.allexecs = set()
214         self.execs = set()
215
216     def parse_shell(self, value):
217         """Parse the supplied shell code in a string, returning the external
218         commands it executes.
219         """
220
221         h = hash(str(value))
222
223         if h in shellparsecache:
224             self.execs = shellparsecache[h]["execs"]
225             return self.execs
226
227         try:
228             tokens, _ = pyshyacc.parse(value, eof=True, debug=False)
229         except pyshlex.NeedMore:
230             raise sherrors.ShellSyntaxError("Unexpected EOF")
231
232         for token in tokens:
233             self.process_tokens(token)
234         self.execs = set(cmd for cmd in self.allexecs if cmd not in self.funcdefs)
235
236         shellparsecache[h] = {}
237         shellparsecache[h]["execs"] = self.execs
238
239         return self.execs
240
241     def process_tokens(self, tokens):
242         """Process a supplied portion of the syntax tree as returned by
243         pyshyacc.parse.
244         """
245
246         def function_definition(value):
247             self.funcdefs.add(value.name)
248             return [value.body], None
249
250         def case_clause(value):
251             # Element 0 of each item in the case is the list of patterns, and
252             # Element 1 of each item in the case is the list of commands to be
253             # executed when that pattern matches.
254             words = chain(*[item[0] for item in value.items])
255             cmds  = chain(*[item[1] for item in value.items])
256             return cmds, words
257
258         def if_clause(value):
259             main = chain(value.cond, value.if_cmds)
260             rest = value.else_cmds
261             if isinstance(rest, tuple) and rest[0] == "elif":
262                 return chain(main, if_clause(rest[1]))
263             else:
264                 return chain(main, rest)
265
266         def simple_command(value):
267             return None, chain(value.words, (assign[1] for assign in value.assigns))
268
269         token_handlers = {
270             "and_or": lambda x: ((x.left, x.right), None),
271             "async": lambda x: ([x], None),
272             "brace_group": lambda x: (x.cmds, None),
273             "for_clause": lambda x: (x.cmds, x.items),
274             "function_definition": function_definition,
275             "if_clause": lambda x: (if_clause(x), None),
276             "pipeline": lambda x: (x.commands, None),
277             "redirect_list": lambda x: ([x.cmd], None),
278             "subshell": lambda x: (x.cmds, None),
279             "while_clause": lambda x: (chain(x.condition, x.cmds), None),
280             "until_clause": lambda x: (chain(x.condition, x.cmds), None),
281             "simple_command": simple_command,
282             "case_clause": case_clause,
283         }
284
285         for token in tokens:
286             name, value = token
287             try:
288                 more_tokens, words = token_handlers[name](value)
289             except KeyError:
290                 raise NotImplementedError("Unsupported token type " + name)
291
292             if more_tokens:
293                 self.process_tokens(more_tokens)
294
295             if words:
296                 self.process_words(words)
297
298     def process_words(self, words):
299         """Process a set of 'words' in pyshyacc parlance, which includes
300         extraction of executed commands from $() blocks, as well as grabbing
301         the command name argument.
302         """
303
304         words = list(words)
305         for word in list(words):
306             wtree = pyshlex.make_wordtree(word[1])
307             for part in wtree:
308                 if not isinstance(part, list):
309                     continue
310
311                 if part[0] in ('`', '$('):
312                     command = pyshlex.wordtree_as_string(part[1:-1])
313                     self.parse_shell(command)
314
315                     if word[0] in ("cmd_name", "cmd_word"):
316                         if word in words:
317                             words.remove(word)
318
319         usetoken = False
320         for word in words:
321             if word[0] in ("cmd_name", "cmd_word") or \
322                (usetoken and word[0] == "TOKEN"):
323                 if "=" in word[1]:
324                     usetoken = True
325                     continue
326
327                 cmd = word[1]
328                 if cmd.startswith("$"):
329                     logger.debug(1, "Warning: execution of non-literal "
330                                     "command '%s'", cmd)
331                 elif cmd == "eval":
332                     command = " ".join(word for _, word in words[1:])
333                     self.parse_shell(command)
334                 else:
335                     self.allexecs.add(cmd)
336                 break