1#!/usr/bin/env python
2#
3# Copyright (C) 2012 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Generates default implementations of operator<< for enum types."""
18
19import codecs
20import os
21import re
22import string
23import sys
24
25
26_ENUM_START_RE = re.compile(r'\benum\b\s+(class\s+)?(\S+)\s+:?.*\{(\s+// private)?')
27_ENUM_VALUE_RE = re.compile(r'([A-Za-z0-9_]+)(.*)')
28_ENUM_END_RE = re.compile(r'^\s*\};$')
29_ENUMS = {}
30_NAMESPACES = {}
31_ENUM_CLASSES = {}
32
33def Confused(filename, line_number, line):
34  sys.stderr.write('%s:%d: confused by:\n%s\n' % (filename, line_number, line))
35  raise Exception("giving up!")
36  sys.exit(1)
37
38
39def ProcessFile(filename):
40  lines = codecs.open(filename, 'r', 'utf8', 'replace').read().split('\n')
41  in_enum = False
42  is_enum_private = False
43  is_enum_class = False
44  line_number = 0
45
46
47  namespaces = []
48  enclosing_classes = []
49
50  for raw_line in lines:
51    line_number += 1
52
53    if not in_enum:
54      # Is this the start of a new enum?
55      m = _ENUM_START_RE.search(raw_line)
56      if m:
57        # Yes, so add an empty entry to _ENUMS for this enum.
58
59        # Except when it's private
60        if m.group(3) is not None:
61          is_enum_private = True
62        else:
63          is_enum_private = False
64          is_enum_class = m.group(1) is not None
65          enum_name = m.group(2)
66          if len(enclosing_classes) > 0:
67            enum_name = '::'.join(enclosing_classes) + '::' + enum_name
68          _ENUMS[enum_name] = []
69          _NAMESPACES[enum_name] = '::'.join(namespaces)
70          _ENUM_CLASSES[enum_name] = is_enum_class
71        in_enum = True
72        continue
73
74      # Is this the start or end of a namespace?
75      m = re.compile(r'^namespace (\S+) \{').search(raw_line)
76      if m:
77        namespaces.append(m.group(1))
78        continue
79      m = re.compile(r'^\}\s+// namespace').search(raw_line)
80      if m:
81        namespaces = namespaces[0:len(namespaces) - 1]
82        continue
83
84      # Is this the start or end of an enclosing class or struct?
85      m = re.compile(r'^\s*(?:class|struct)(?: MANAGED)?(?: PACKED\([0-9]\))? (\S+).* \{').search(raw_line)
86      if m:
87        enclosing_classes.append(m.group(1))
88        continue
89
90      # End of class/struct -- be careful not to match "do { ... } while" constructs by accident
91      m = re.compile(r'^\s*\}(\s+)?(while)?(.+)?;').search(raw_line)
92      if m and not m.group(2):
93        enclosing_classes = enclosing_classes[0:len(enclosing_classes) - 1]
94        continue
95
96      continue
97
98    # Is this the end of the current enum?
99    m = _ENUM_END_RE.search(raw_line)
100    if m:
101      if not in_enum:
102        Confused(filename, line_number, raw_line)
103      in_enum = False
104      continue
105
106    if is_enum_private:
107      continue
108
109    # The only useful thing in comments is the <<alternate text>> syntax for
110    # overriding the default enum value names. Pull that out...
111    enum_text = None
112    m_comment = re.compile(r'// <<(.*?)>>').search(raw_line)
113    if m_comment:
114      enum_text = m_comment.group(1)
115    # ...and then strip // comments.
116    line = re.sub(r'//.*', '', raw_line)
117
118    # Strip whitespace.
119    line = line.strip()
120
121    # Skip blank lines.
122    if len(line) == 0:
123      continue
124
125    # Since we know we're in an enum type, and we're not looking at a comment
126    # or a blank line, this line should be the next enum value...
127    m = _ENUM_VALUE_RE.search(line)
128    if not m:
129      Confused(filename, line_number, raw_line)
130    enum_value = m.group(1)
131
132    # By default, we turn "kSomeValue" into "SomeValue".
133    if enum_text == None:
134      enum_text = enum_value
135      if enum_text.startswith('k'):
136        enum_text = enum_text[1:]
137
138    # Lose literal values because we don't care; turn "= 123, // blah" into ", // blah".
139    rest = m.group(2).strip()
140    m_literal = re.compile(r'= (0x[0-9a-f]+|-?[0-9]+|\'.\')').search(rest)
141    if m_literal:
142      rest = rest[(len(m_literal.group(0))):]
143
144    # With "kSomeValue = kOtherValue," we take the original and skip later synonyms.
145    # TODO: check that the rhs is actually an existing value.
146    if rest.startswith('= k'):
147      continue
148
149    # Remove any trailing comma and whitespace
150    if rest.startswith(','):
151      rest = rest[1:]
152    rest = rest.strip()
153
154    # There shouldn't be anything left.
155    if len(rest):
156      sys.stderr.write('%s\n' % (rest))
157      Confused(filename, line_number, raw_line)
158
159    # If the enum is scoped, we must prefix enum value with enum name (which is already prefixed
160    # by enclosing classes).
161    if is_enum_class:
162      enum_value = enum_name + '::' + enum_value
163    else:
164      if len(enclosing_classes) > 0:
165        enum_value = '::'.join(enclosing_classes) + '::' + enum_value
166
167    _ENUMS[enum_name].append((enum_value, enum_text))
168
169def main():
170  local_path = sys.argv[1]
171  header_files = []
172  for header_file in sys.argv[2:]:
173    header_files.append(header_file)
174    ProcessFile(header_file)
175
176  print('#include <iostream>')
177  print('')
178
179  for header_file in header_files:
180    header_file = header_file.replace(local_path + '/', '')
181    print('#include "%s"' % header_file)
182
183  print('')
184
185  for enum_name in _ENUMS:
186    print('// This was automatically generated by %s --- do not edit!' % sys.argv[0])
187
188    namespaces = _NAMESPACES[enum_name].split('::')
189    for namespace in namespaces:
190      print('namespace %s {' % namespace)
191
192    print('std::ostream& operator<<(std::ostream& os, const %s& rhs) {' % enum_name)
193    print('  switch (rhs) {')
194    for (enum_value, enum_text) in _ENUMS[enum_name]:
195      print('    case %s: os << "%s"; break;' % (enum_value, enum_text))
196    if not _ENUM_CLASSES[enum_name]:
197      print('    default: os << "%s[" << static_cast<int>(rhs) << "]"; break;' % enum_name)
198    print('  }')
199    print('  return os;')
200    print('}')
201
202    for namespace in reversed(namespaces):
203      print('}  // namespace %s' % namespace)
204    print('')
205
206  sys.exit(0)
207
208
209if __name__ == '__main__':
210  main()
211