idl_node.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Nodes for PPAPI IDL AST"""
7
8#
9# IDL Node
10#
11# IDL Node defines the IDLAttribute and IDLNode objects which are constructed
12# by the parser as it processes the various 'productions'.  The IDLAttribute
13# objects are assigned to the IDLNode's property dictionary instead of being
14# applied as children of The IDLNodes, so they do not exist in the final tree.
15# The AST of IDLNodes is the output from the parsing state and will be used
16# as the source data by the various generators.
17#
18
19import hashlib
20import sys
21
22from idl_log import ErrOut, InfoOut, WarnOut
23from idl_propertynode import IDLPropertyNode
24from idl_namespace import IDLNamespace
25from idl_release import IDLRelease, IDLReleaseMap
26
27
28# IDLAttribute
29#
30# A temporary object used by the parsing process to hold an Extended Attribute
31# which will be passed as a child to a standard IDLNode.
32#
33class IDLAttribute(object):
34  def __init__(self, name, value):
35    self.cls = 'ExtAttribute'
36    self.name = name
37    self.value = value
38
39  def __str__(self):
40    return '%s=%s' % (self.name, self.value)
41
42#
43# IDLNode
44#
45# This class implements the AST tree, providing the associations between
46# parents and children.  It also contains a namepsace and propertynode to
47# allow for look-ups.  IDLNode is derived from IDLRelease, so it is
48# version aware.
49#
50class IDLNode(IDLRelease):
51
52  # Set of object IDLNode types which have a name and belong in the namespace.
53  NamedSet = set(['Enum', 'EnumItem', 'File', 'Function', 'Interface',
54                  'Member', 'Param', 'Struct', 'Type', 'Typedef'])
55
56  show_versions = False
57  def __init__(self, cls, filename, lineno, pos, children=None):
58    # Initialize with no starting or ending Version
59    IDLRelease.__init__(self, None, None)
60
61    self.cls = cls
62    self.lineno = lineno
63    self.pos = pos
64    self.filename = filename
65    self.filenode = None
66    self.hashes = {}
67    self.deps = {}
68    self.errors = 0
69    self.namespace = None
70    self.typelist = None
71    self.parent = None
72    self.property_node = IDLPropertyNode()
73
74    # A list of unique releases for this node
75    self.releases = None
76
77    # A map from any release, to the first unique release
78    self.first_release = None
79
80    # self.children is a list of children ordered as defined
81    self.children = []
82    # Process the passed in list of children, placing ExtAttributes into the
83    # property dictionary, and nodes into the local child list in order.  In
84    # addition, add nodes to the namespace if the class is in the NamedSet.
85    if not children: children = []
86    for child in children:
87      if child.cls == 'ExtAttribute':
88        self.SetProperty(child.name, child.value)
89      else:
90        self.AddChild(child)
91
92#
93# String related functions
94#
95#
96
97  # Return a string representation of this node
98  def __str__(self):
99    name = self.GetName()
100    ver = IDLRelease.__str__(self)
101    if name is None: name = ''
102    if not IDLNode.show_versions: ver = ''
103    return '%s(%s%s)' % (self.cls, name, ver)
104
105  # Return file and line number for where node was defined
106  def Location(self):
107    return '%s(%d)' % (self.filename, self.lineno)
108
109  # Log an error for this object
110  def Error(self, msg):
111    self.errors += 1
112    ErrOut.LogLine(self.filename, self.lineno, 0, ' %s %s' %
113                   (str(self), msg))
114    if self.filenode:
115      errcnt = self.filenode.GetProperty('ERRORS', 0)
116      self.filenode.SetProperty('ERRORS', errcnt + 1)
117
118  # Log a warning for this object
119  def Warning(self, msg):
120    WarnOut.LogLine(self.filename, self.lineno, 0, ' %s %s' %
121                    (str(self), msg))
122
123  def GetName(self):
124    return self.GetProperty('NAME')
125
126  def GetNameVersion(self):
127    name = self.GetProperty('NAME', default='')
128    ver = IDLRelease.__str__(self)
129    return '%s%s' % (name, ver)
130
131  # Dump this object and its children
132  def Dump(self, depth=0, comments=False, out=sys.stdout):
133    if self.cls in ['Comment', 'Copyright']:
134      is_comment = True
135    else:
136      is_comment = False
137
138    # Skip this node if it's a comment, and we are not printing comments
139    if not comments and is_comment: return
140
141    tab = ''.rjust(depth * 2)
142    if is_comment:
143      out.write('%sComment\n' % tab)
144      for line in self.GetName().split('\n'):
145        out.write('%s  "%s"\n' % (tab, line))
146    else:
147      ver = IDLRelease.__str__(self)
148      if self.releases:
149        release_list = ': ' + ' '.join(self.releases)
150      else:
151        release_list = ': undefined'
152      out.write('%s%s%s%s\n' % (tab, self, ver, release_list))
153    if self.typelist:
154      out.write('%s  Typelist: %s\n' % (tab, self.typelist.GetReleases()[0]))
155    properties = self.property_node.GetPropertyList()
156    if properties:
157      out.write('%s  Properties\n' % tab)
158      for p in properties:
159        if is_comment and p == 'NAME':
160          # Skip printing the name for comments, since we printed above already
161          continue
162        out.write('%s    %s : %s\n' % (tab, p, self.GetProperty(p)))
163    for child in self.children:
164      child.Dump(depth+1, comments=comments, out=out)
165
166#
167# Search related functions
168#
169  # Check if node is of a given type
170  def IsA(self, *typelist):
171    if self.cls in typelist: return True
172    return False
173
174  # Get a list of objects for this key
175  def GetListOf(self, *keys):
176    out = []
177    for child in self.children:
178      if child.cls in keys: out.append(child)
179    return out
180
181  def GetOneOf(self, *keys):
182    out = self.GetListOf(*keys)
183    if out: return out[0]
184    return None
185
186  def SetParent(self, parent):
187    self.property_node.AddParent(parent)
188    self.parent = parent
189
190  def AddChild(self, node):
191    node.SetParent(self)
192    self.children.append(node)
193
194  # Get a list of all children
195  def GetChildren(self):
196    return self.children
197
198  # Get a list of all children of a given version
199  def GetChildrenVersion(self, version):
200    out = []
201    for child in self.children:
202      if child.IsVersion(version): out.append(child)
203    return out
204
205  # Get a list of all children in a given range
206  def GetChildrenRange(self, vmin, vmax):
207    out = []
208    for child in self.children:
209      if child.IsRange(vmin, vmax): out.append(child)
210    return out
211
212  def FindVersion(self, name, version):
213    node = self.namespace.FindNode(name, version)
214    if not node and self.parent:
215      node = self.parent.FindVersion(name, version)
216    return node
217
218  def FindRange(self, name, vmin, vmax):
219    nodes = self.namespace.FindNodes(name, vmin, vmax)
220    if not nodes and self.parent:
221      nodes = self.parent.FindVersion(name, vmin, vmax)
222    return nodes
223
224  def GetType(self, release):
225    if not self.typelist: return None
226    return self.typelist.FindRelease(release)
227
228  def GetHash(self, release):
229    hashval = self.hashes.get(release, None)
230    if hashval is None:
231      hashval = hashlib.sha1()
232      hashval.update(self.cls)
233      for key in self.property_node.GetPropertyList():
234        val = self.GetProperty(key)
235        hashval.update('%s=%s' % (key, str(val)))
236      typeref = self.GetType(release)
237      if typeref:
238        hashval.update(typeref.GetHash(release))
239      for child in self.GetChildren():
240        if child.IsA('Copyright', 'Comment', 'Label'): continue
241        if not child.IsRelease(release):
242          continue
243        hashval.update( child.GetHash(release) )
244      self.hashes[release] = hashval
245    return hashval.hexdigest()
246
247  def GetDeps(self, release, visited=None):
248    visited = visited or set()
249
250    # If this release is not valid for this object, then done.
251    if not self.IsRelease(release) or self.IsA('Comment', 'Copyright'):
252      return set([])
253
254    # If we have cached the info for this release, return the cached value
255    deps = self.deps.get(release, None)
256    if deps is not None:
257      return deps
258
259    # If we are already visited, then return
260    if self in visited:
261      return set([self])
262
263    # Otherwise, build the dependency list
264    visited |= set([self])
265    deps = set([self])
266
267    # Get child deps
268    for child in self.GetChildren():
269      deps |= child.GetDeps(release, visited)
270      visited |= set(deps)
271
272    # Get type deps
273    typeref = self.GetType(release)
274    if typeref:
275      deps |= typeref.GetDeps(release, visited)
276
277    self.deps[release] = deps
278    return deps
279
280  def GetVersion(self, release):
281    filenode = self.GetProperty('FILE')
282    if not filenode:
283      return None
284    return filenode.release_map.GetVersion(release)
285
286  def GetUniqueReleases(self, releases):
287    """Return the unique set of first releases corresponding to input
288
289    Since we are returning the corresponding 'first' version for a
290    release, we may return a release version prior to the one in the list."""
291    my_min, my_max = self.GetMinMax(releases)
292    if my_min > releases[-1] or my_max < releases[0]:
293      return []
294
295    out = set()
296    for rel in releases:
297      remapped = self.first_release[rel]
298      if not remapped: continue
299      out |= set([remapped])
300    out = sorted(out)
301    return out
302
303
304  def GetRelease(self, version):
305    filenode = self.GetProperty('FILE')
306    if not filenode:
307      return None
308    return filenode.release_map.GetRelease(version)
309
310  def _GetReleases(self, releases):
311    if not self.releases:
312      my_min, my_max = self.GetMinMax(releases)
313      my_releases = [my_min]
314      if my_max != releases[-1]:
315        my_releases.append(my_max)
316      my_releases = set(my_releases)
317      for child in self.GetChildren():
318        if child.IsA('Copyright', 'Comment', 'Label'):
319          continue
320        my_releases |= child.GetReleases(releases)
321      self.releases = my_releases
322    return self.releases
323
324
325  def _GetReleaseList(self, releases, visited=None):
326    visited = visited or set()
327    if not self.releases:
328      # If we are unversionable, then return first available release
329      if self.IsA('Comment', 'Copyright', 'Label'):
330        self.releases = []
331        return self.releases
332
333      # Generate the first and if deprecated within this subset, the
334      # last release for this node
335      my_min, my_max = self.GetMinMax(releases)
336
337      if my_max != releases[-1]:
338        my_releases = set([my_min, my_max])
339      else:
340        my_releases = set([my_min])
341
342      # Break cycle if we reference ourselves
343      if self in visited:
344        return [my_min]
345
346      visited |= set([self])
347
348      # Files inherit all their releases from items in the file
349      if self.IsA('AST', 'File'):
350        my_releases = set()
351
352      # Visit all children
353      child_releases = set()
354
355      # Exclude sibling results from parent visited set
356      cur_visits = visited
357
358      for child in self.children:
359        child_releases |= set(child._GetReleaseList(releases, cur_visits))
360        visited |= set(child_releases)
361
362      # Visit my type
363      type_releases = set()
364      if self.typelist:
365        type_list = self.typelist.GetReleases()
366        for typenode in type_list:
367          type_releases |= set(typenode._GetReleaseList(releases, cur_visits))
368
369        type_release_list = sorted(type_releases)
370        if my_min < type_release_list[0]:
371          type_node = type_list[0]
372          self.Error('requires %s in %s which is undefined at %s.' % (
373              type_node, type_node.filename, my_min))
374
375      for rel in child_releases | type_releases:
376        if rel >= my_min and rel <= my_max:
377          my_releases |= set([rel])
378
379      self.releases = sorted(my_releases)
380    return self.releases
381
382  def GetReleaseList(self):
383    return self.releases
384
385  def BuildReleaseMap(self, releases):
386    unique_list = self._GetReleaseList(releases)
387    my_min, my_max = self.GetMinMax(releases)
388
389    self.first_release = {}
390    last_rel = None
391    for rel in releases:
392      if rel in unique_list:
393        last_rel = rel
394      self.first_release[rel] = last_rel
395      if rel == my_max:
396        last_rel = None
397
398  def SetProperty(self, name, val):
399    self.property_node.SetProperty(name, val)
400
401  def GetProperty(self, name, default=None):
402    return self.property_node.GetProperty(name, default)
403
404  def Traverse(self, data, func):
405    func(self, data)
406    for child in self.children:
407      child.Traverse(data, func)
408
409
410#
411# IDLFile
412#
413# A specialized version of IDLNode which tracks errors and warnings.
414#
415class IDLFile(IDLNode):
416  def __init__(self, name, children, errors=0):
417    attrs = [IDLAttribute('NAME', name),
418             IDLAttribute('ERRORS', errors)]
419    if not children: children = []
420    IDLNode.__init__(self, 'File', name, 1, 0, attrs + children)
421    self.release_map = IDLReleaseMap([('M13', 1.0)])
422
423
424#
425# Tests
426#
427def StringTest():
428  errors = 0
429  name_str = 'MyName'
430  text_str = 'MyNode(%s)' % name_str
431  name_node = IDLAttribute('NAME', name_str)
432  node = IDLNode('MyNode', 'no file', 1, 0, [name_node])
433  if node.GetName() != name_str:
434    ErrOut.Log('GetName returned >%s< not >%s<' % (node.GetName(), name_str))
435    errors += 1
436  if node.GetProperty('NAME') != name_str:
437    ErrOut.Log('Failed to get name property.')
438    errors += 1
439  if str(node) != text_str:
440    ErrOut.Log('str() returned >%s< not >%s<' % (str(node), text_str))
441    errors += 1
442  if not errors: InfoOut.Log('Passed StringTest')
443  return errors
444
445
446def ChildTest():
447  errors = 0
448  child = IDLNode('child', 'no file', 1, 0)
449  parent = IDLNode('parent', 'no file', 1, 0, [child])
450
451  if child.parent != parent:
452    ErrOut.Log('Failed to connect parent.')
453    errors += 1
454
455  if [child] != parent.GetChildren():
456    ErrOut.Log('Failed GetChildren.')
457    errors += 1
458
459  if child != parent.GetOneOf('child'):
460    ErrOut.Log('Failed GetOneOf(child)')
461    errors += 1
462
463  if parent.GetOneOf('bogus'):
464    ErrOut.Log('Failed GetOneOf(bogus)')
465    errors += 1
466
467  if not parent.IsA('parent'):
468    ErrOut.Log('Expecting parent type')
469    errors += 1
470
471  parent = IDLNode('parent', 'no file', 1, 0, [child, child])
472  if [child, child] != parent.GetChildren():
473    ErrOut.Log('Failed GetChildren2.')
474    errors += 1
475
476  if not errors: InfoOut.Log('Passed ChildTest')
477  return errors
478
479
480def Main():
481  errors = StringTest()
482  errors += ChildTest()
483
484  if errors:
485    ErrOut.Log('IDLNode failed with %d errors.' % errors)
486    return  -1
487  return 0
488
489if __name__ == '__main__':
490  sys.exit(Main())
491
492