| #!/usr/bin/env python | |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Nodes for PPAPI IDL AST""" | |
| # | |
| # IDL Node | |
| # | |
| # IDL Node defines the IDLAttribute and IDLNode objects which are constructed | |
| # by the parser as it processes the various 'productions'. The IDLAttribute | |
| # objects are assigned to the IDLNode's property dictionary instead of being | |
| # applied as children of The IDLNodes, so they do not exist in the final tree. | |
| # The AST of IDLNodes is the output from the parsing state and will be used | |
| # as the source data by the various generators. | |
| # | |
| import sys | |
| from idl_log import ErrOut, InfoOut, WarnOut | |
| from idl_propertynode import IDLPropertyNode | |
| from idl_release import IDLRelease, IDLReleaseMap | |
| # IDLAttribute | |
| # | |
| # A temporary object used by the parsing process to hold an Extended Attribute | |
| # which will be passed as a child to a standard IDLNode. | |
| # | |
| class IDLAttribute(object): | |
| def __init__(self, name, value): | |
| self.cls = 'ExtAttribute' | |
| self.name = name | |
| self.value = value | |
| def __str__(self): | |
| return '%s=%s' % (self.name, self.value) | |
| # | |
| # IDLNode | |
| # | |
| # This class implements the AST tree, providing the associations between | |
| # parents and children. It also contains a namepsace and propertynode to | |
| # allow for look-ups. IDLNode is derived from IDLRelease, so it is | |
| # version aware. | |
| # | |
| class IDLNode(IDLRelease): | |
| # Set of object IDLNode types which have a name and belong in the namespace. | |
| NamedSet = set(['Enum', 'EnumItem', 'File', 'Function', 'Interface', | |
| 'Member', 'Param', 'Struct', 'Type', 'Typedef']) | |
| def __init__(self, cls, filename, lineno, pos, children=None): | |
| # Initialize with no starting or ending Version | |
| IDLRelease.__init__(self, None, None) | |
| self.cls = cls | |
| self.lineno = lineno | |
| self.pos = pos | |
| self._filename = filename | |
| self._deps = {} | |
| self.errors = 0 | |
| self.namespace = None | |
| self.typelist = None | |
| self.parent = None | |
| self._property_node = IDLPropertyNode() | |
| self._unique_releases = None | |
| # A list of unique releases for this node | |
| self.releases = None | |
| # A map from any release, to the first unique release | |
| self.first_release = None | |
| # self._children is a list of children ordered as defined | |
| self._children = [] | |
| # Process the passed in list of children, placing ExtAttributes into the | |
| # property dictionary, and nodes into the local child list in order. In | |
| # addition, add nodes to the namespace if the class is in the NamedSet. | |
| if children: | |
| for child in children: | |
| if child.cls == 'ExtAttribute': | |
| self.SetProperty(child.name, child.value) | |
| else: | |
| self.AddChild(child) | |
| def __str__(self): | |
| name = self.GetName() | |
| if name is None: | |
| name = '' | |
| return '%s(%s)' % (self.cls, name) | |
| def Location(self): | |
| """Return a file and line number for where this node was defined.""" | |
| return '%s(%d)' % (self._filename, self.lineno) | |
| def Error(self, msg): | |
| """Log an error for this object.""" | |
| self.errors += 1 | |
| ErrOut.LogLine(self._filename, self.lineno, 0, ' %s %s' % | |
| (str(self), msg)) | |
| filenode = self.GetProperty('FILE') | |
| if filenode: | |
| errcnt = filenode.GetProperty('ERRORS') | |
| if not errcnt: | |
| errcnt = 0 | |
| filenode.SetProperty('ERRORS', errcnt + 1) | |
| def Warning(self, msg): | |
| """Log a warning for this object.""" | |
| WarnOut.LogLine(self._filename, self.lineno, 0, ' %s %s' % | |
| (str(self), msg)) | |
| def GetName(self): | |
| return self.GetProperty('NAME') | |
| def Dump(self, depth=0, comments=False, out=sys.stdout): | |
| """Dump this object and its children""" | |
| if self.cls in ['Comment', 'Copyright']: | |
| is_comment = True | |
| else: | |
| is_comment = False | |
| # Skip this node if it's a comment, and we are not printing comments | |
| if not comments and is_comment: | |
| return | |
| tab = ''.rjust(depth * 2) | |
| if is_comment: | |
| out.write('%sComment\n' % tab) | |
| for line in self.GetName().split('\n'): | |
| out.write('%s "%s"\n' % (tab, line)) | |
| else: | |
| ver = IDLRelease.__str__(self) | |
| if self.releases: | |
| release_list = ': ' + ' '.join(self.releases) | |
| else: | |
| release_list = ': undefined' | |
| out.write('%s%s%s%s\n' % (tab, self, ver, release_list)) | |
| if self.typelist: | |
| out.write('%s Typelist: %s\n' % (tab, self.typelist.GetReleases()[0])) | |
| properties = self._property_node.GetPropertyList() | |
| if properties: | |
| out.write('%s Properties\n' % tab) | |
| for p in properties: | |
| if is_comment and p == 'NAME': | |
| # Skip printing the name for comments, since we printed above already | |
| continue | |
| out.write('%s %s : %s\n' % (tab, p, self.GetProperty(p))) | |
| for child in self._children: | |
| child.Dump(depth+1, comments=comments, out=out) | |
| def IsA(self, *typelist): | |
| """Check if node is of a given type.""" | |
| return self.cls in typelist | |
| def GetListOf(self, *keys): | |
| """Get a list of objects for the given key(s).""" | |
| out = [] | |
| for child in self._children: | |
| if child.cls in keys: | |
| out.append(child) | |
| return out | |
| def GetOneOf(self, *keys): | |
| """Get an object for the given key(s).""" | |
| out = self.GetListOf(*keys) | |
| if out: | |
| return out[0] | |
| return None | |
| def SetParent(self, parent): | |
| self._property_node.AddParent(parent) | |
| self.parent = parent | |
| def AddChild(self, node): | |
| node.SetParent(self) | |
| self._children.append(node) | |
| # Get a list of all children | |
| def GetChildren(self): | |
| return self._children | |
| def GetType(self, release): | |
| if not self.typelist: | |
| return None | |
| return self.typelist.FindRelease(release) | |
| def GetDeps(self, release, visited=None): | |
| visited = visited or set() | |
| # If this release is not valid for this object, then done. | |
| if not self.IsRelease(release) or self.IsA('Comment', 'Copyright'): | |
| return set([]) | |
| # If we have cached the info for this release, return the cached value | |
| deps = self._deps.get(release, None) | |
| if deps is not None: | |
| return deps | |
| # If we are already visited, then return | |
| if self in visited: | |
| return set([self]) | |
| # Otherwise, build the dependency list | |
| visited |= set([self]) | |
| deps = set([self]) | |
| # Get child deps | |
| for child in self.GetChildren(): | |
| deps |= child.GetDeps(release, visited) | |
| visited |= set(deps) | |
| # Get type deps | |
| typeref = self.GetType(release) | |
| if typeref: | |
| deps |= typeref.GetDeps(release, visited) | |
| self._deps[release] = deps | |
| return deps | |
| def GetVersion(self, release): | |
| filenode = self.GetProperty('FILE') | |
| if not filenode: | |
| return None | |
| return filenode.release_map.GetVersion(release) | |
| def GetUniqueReleases(self, releases): | |
| """Return the unique set of first releases corresponding to input | |
| Since we are returning the corresponding 'first' version for a | |
| release, we may return a release version prior to the one in the list.""" | |
| my_min, my_max = self.GetMinMax(releases) | |
| if my_min > releases[-1] or my_max < releases[0]: | |
| return [] | |
| out = set() | |
| for rel in releases: | |
| remapped = self.first_release[rel] | |
| if not remapped: | |
| continue | |
| out |= set([remapped]) | |
| # Cache the most recent set of unique_releases | |
| self._unique_releases = sorted(out) | |
| return self._unique_releases | |
| def LastRelease(self, release): | |
| # Get the most recent release from the most recently generated set of | |
| # cached unique releases. | |
| if self._unique_releases and self._unique_releases[-1] > release: | |
| return False | |
| return True | |
| def GetRelease(self, version): | |
| filenode = self.GetProperty('FILE') | |
| if not filenode: | |
| return None | |
| return filenode.release_map.GetRelease(version) | |
| def _GetReleaseList(self, releases, visited=None): | |
| visited = visited or set() | |
| if not self.releases: | |
| # If we are unversionable, then return first available release | |
| if self.IsA('Comment', 'Copyright', 'Label'): | |
| self.releases = [] | |
| return self.releases | |
| # Generate the first and if deprecated within this subset, the | |
| # last release for this node | |
| my_min, my_max = self.GetMinMax(releases) | |
| if my_max != releases[-1]: | |
| my_releases = set([my_min, my_max]) | |
| else: | |
| my_releases = set([my_min]) | |
| r = self.GetRelease(self.GetProperty('version')) | |
| if not r in my_releases: | |
| my_releases |= set([r]) | |
| # Break cycle if we reference ourselves | |
| if self in visited: | |
| return [my_min] | |
| visited |= set([self]) | |
| # Files inherit all their releases from items in the file | |
| if self.IsA('AST', 'File'): | |
| my_releases = set() | |
| # Visit all children | |
| child_releases = set() | |
| # Exclude sibling results from parent visited set | |
| cur_visits = visited | |
| for child in self._children: | |
| child_releases |= set(child._GetReleaseList(releases, cur_visits)) | |
| visited |= set(child_releases) | |
| # Visit my type | |
| type_releases = set() | |
| if self.typelist: | |
| type_list = self.typelist.GetReleases() | |
| for typenode in type_list: | |
| type_releases |= set(typenode._GetReleaseList(releases, cur_visits)) | |
| type_release_list = sorted(type_releases) | |
| if my_min < type_release_list[0]: | |
| type_node = type_list[0] | |
| self.Error('requires %s in %s which is undefined at %s.' % ( | |
| type_node, type_node._filename, my_min)) | |
| for rel in child_releases | type_releases: | |
| if rel >= my_min and rel <= my_max: | |
| my_releases |= set([rel]) | |
| self.releases = sorted(my_releases) | |
| return self.releases | |
| def BuildReleaseMap(self, releases): | |
| unique_list = self._GetReleaseList(releases) | |
| _, my_max = self.GetMinMax(releases) | |
| self.first_release = {} | |
| last_rel = None | |
| for rel in releases: | |
| if rel in unique_list: | |
| last_rel = rel | |
| self.first_release[rel] = last_rel | |
| if rel == my_max: | |
| last_rel = None | |
| def SetProperty(self, name, val): | |
| self._property_node.SetProperty(name, val) | |
| def GetProperty(self, name): | |
| return self._property_node.GetProperty(name) | |
| def GetPropertyLocal(self, name): | |
| return self._property_node.GetPropertyLocal(name) | |
| def NodeIsDevOnly(self): | |
| """Returns true iff a node is only in dev channel.""" | |
| return self.GetProperty('dev_version') and not self.GetProperty('version') | |
| def DevInterfaceMatchesStable(self, release): | |
| """Returns true if an interface has an equivalent stable version.""" | |
| assert(self.IsA('Interface')) | |
| for child in self.GetListOf('Member'): | |
| unique = child.GetUniqueReleases([release]) | |
| if not unique or not child.InReleases([release]): | |
| continue | |
| if child.NodeIsDevOnly(): | |
| return False | |
| return True | |
| # | |
| # IDLFile | |
| # | |
| # A specialized version of IDLNode which tracks errors and warnings. | |
| # | |
| class IDLFile(IDLNode): | |
| def __init__(self, name, children, errors=0): | |
| attrs = [IDLAttribute('NAME', name), | |
| IDLAttribute('ERRORS', errors)] | |
| if not children: | |
| children = [] | |
| IDLNode.__init__(self, 'File', name, 1, 0, attrs + children) | |
| # TODO(teravest): Why do we set release map like this here? This looks | |
| # suspicious... | |
| self.release_map = IDLReleaseMap([('M13', 1.0, 'stable')]) | |
| # | |
| # Tests | |
| # | |
| def StringTest(): | |
| errors = 0 | |
| name_str = 'MyName' | |
| text_str = 'MyNode(%s)' % name_str | |
| name_node = IDLAttribute('NAME', name_str) | |
| node = IDLNode('MyNode', 'no file', 1, 0, [name_node]) | |
| if node.GetName() != name_str: | |
| ErrOut.Log('GetName returned >%s< not >%s<' % (node.GetName(), name_str)) | |
| errors += 1 | |
| if node.GetProperty('NAME') != name_str: | |
| ErrOut.Log('Failed to get name property.') | |
| errors += 1 | |
| if str(node) != text_str: | |
| ErrOut.Log('str() returned >%s< not >%s<' % (str(node), text_str)) | |
| errors += 1 | |
| if not errors: | |
| InfoOut.Log('Passed StringTest') | |
| return errors | |
| def ChildTest(): | |
| errors = 0 | |
| child = IDLNode('child', 'no file', 1, 0) | |
| parent = IDLNode('parent', 'no file', 1, 0, [child]) | |
| if child.parent != parent: | |
| ErrOut.Log('Failed to connect parent.') | |
| errors += 1 | |
| if [child] != parent.GetChildren(): | |
| ErrOut.Log('Failed GetChildren.') | |
| errors += 1 | |
| if child != parent.GetOneOf('child'): | |
| ErrOut.Log('Failed GetOneOf(child)') | |
| errors += 1 | |
| if parent.GetOneOf('bogus'): | |
| ErrOut.Log('Failed GetOneOf(bogus)') | |
| errors += 1 | |
| if not parent.IsA('parent'): | |
| ErrOut.Log('Expecting parent type') | |
| errors += 1 | |
| parent = IDLNode('parent', 'no file', 1, 0, [child, child]) | |
| if [child, child] != parent.GetChildren(): | |
| ErrOut.Log('Failed GetChildren2.') | |
| errors += 1 | |
| if not errors: | |
| InfoOut.Log('Passed ChildTest') | |
| return errors | |
| def Main(): | |
| errors = StringTest() | |
| errors += ChildTest() | |
| if errors: | |
| ErrOut.Log('IDLNode failed with %d errors.' % errors) | |
| return -1 | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(Main()) |