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