1#!/usr/bin/env python3
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 re
21import sys
22
23
24_ENUM_START_RE = re.compile(
25    r'\benum\b\s+(class\s+)?(\S+)\s+:?.*\{(\s+// private)?')
26_ENUM_VALUE_RE = re.compile(r'([A-Za-z0-9_]+)(.*)')
27_ENUM_END_RE = re.compile(r'^\s*\};$')
28_ENUMS = {}
29_NAMESPACES = {}
30_ENUM_CLASSES = {}
31
32
33def Confused(filename, line_number, line):
34    sys.stderr.write('%s:%d: confused by:\n%s\n' %
35                     (filename, line_number, line))
36    raise Exception("giving up!")
37    sys.exit(1)
38
39
40def ProcessFile(filename):
41    lines = codecs.open(filename, 'r', 'utf8', 'replace').read().split('\n')
42
43    class EnumLines:
44        def __init__(self, ns, ec):
45            self.namespaces = ns
46            self.enclosing_classes = ec
47            self.lines = []
48
49    def generate_enum_lines(l):
50        line_number = 0
51        enum_lines = None
52        namespaces = []
53        enclosing_classes = []
54
55        for raw_line in l:
56            line_number += 1
57
58            if enum_lines is None:
59                # Is this the start of a new enum?
60                m = _ENUM_START_RE.search(raw_line)
61                if m:
62                    # Yes, so create new line list.
63                    enum_lines = EnumLines(namespaces[:], enclosing_classes[:])
64                    enum_lines.lines.append((raw_line, line_number))
65                    continue
66
67                # Is this the start or end of a namespace?
68                m = re.search(r'^namespace (\S+) \{', raw_line)
69                if m:
70                    namespaces.append(m.group(1))
71                    continue
72                m = re.search(r'^\}\s+// namespace', raw_line)
73                if m:
74                    namespaces = namespaces[0:len(namespaces) - 1]
75                    continue
76
77                # Is this the start or end of an enclosing class or struct?
78                m = re.search(
79                    r'^\s*(?:class|struct)(?: MANAGED)?(?: PACKED\([0-9]\))? (\S+).* \{', raw_line)
80                if m:
81                    enclosing_classes.append(m.group(1))
82                    continue
83
84                # End of class/struct -- be careful not to match "do { ... } while" constructs by accident
85                m = re.search(r'^\s*\}(\s+)?(while)?(.+)?;', raw_line)
86                if m and not m.group(2):
87                    enclosing_classes = enclosing_classes[0:len(enclosing_classes) - 1]
88                    continue
89
90                continue
91
92            # Is this the end of the current enum?
93            m = _ENUM_END_RE.search(raw_line)
94            if m:
95                if enum_lines is None:
96                    Confused(filename, line_number, raw_line)
97                yield enum_lines
98                enum_lines = None
99                continue
100
101            # Append the line
102            enum_lines.lines.append((raw_line, line_number))
103
104    for enum_lines in generate_enum_lines(lines):
105        m = _ENUM_START_RE.search(enum_lines.lines[0][0])
106        if m.group(3) is not None:
107            # Skip private enums.
108            continue
109
110        # Add an empty entry to _ENUMS for this enum.
111        is_enum_class = m.group(1) is not None
112        enum_name = m.group(2)
113        if len(enum_lines.enclosing_classes) > 0:
114            enum_name = '::'.join(enum_lines.enclosing_classes) + '::' + enum_name
115        _ENUMS[enum_name] = []
116        _NAMESPACES[enum_name] = '::'.join(enum_lines.namespaces)
117        _ENUM_CLASSES[enum_name] = is_enum_class
118
119        def generate_non_empty_line(lines):
120            for raw_line, line_number in lines:
121                # Strip // comments.
122                line = re.sub(r'//.*', '', raw_line)
123                # Strip whitespace.
124                line = line.strip()
125                # Skip blank lines.
126                if len(line) == 0:
127                    continue
128
129                # The only useful thing in comments is the <<alternate text>> syntax for
130                # overriding the default enum value names. Pull that out...
131                enum_text = None
132                m_comment = re.search(r'// <<(.*?)>>', raw_line)
133                if m_comment:
134                    enum_text = m_comment.group(1)
135
136                yield (line, enum_text, raw_line, line_number)
137
138        for line, enum_text, raw_line, line_number in generate_non_empty_line(enum_lines.lines[1:]):
139            # Since we know we're in an enum type, and we're not looking at a comment
140            # or a blank line, this line should be the next enum value...
141            m = _ENUM_VALUE_RE.search(line)
142            if not m:
143                Confused(filename, line_number, raw_line)
144            enum_value = m.group(1)
145
146            # By default, we turn "kSomeValue" into "SomeValue".
147            if enum_text is None:
148                enum_text = enum_value
149                if enum_text.startswith('k'):
150                    enum_text = enum_text[1:]
151
152            # Check that we understand the line (and hopefully do not parse incorrectly), or should
153            # filter.
154            rest = m.group(2).strip()
155
156            # With "kSomeValue = kOtherValue," we take the original and skip later synonyms.
157            # TODO: check that the rhs is actually an existing value.
158            if rest.startswith('= k'):
159                continue
160
161            # Remove trailing comma.
162            if rest.endswith(','):
163                rest = rest[:-1]
164
165            # We now expect rest to be empty, or an assignment to an "expression."
166            if len(rest):
167                # We want to lose the expression "= [exp]". As we do not have a real C parser, just
168                # assume anything without a comma is valid.
169                m_exp = re.match('= [^,]+$', rest)
170                if m_exp is None:
171                    sys.stderr.write('%s\n' % (rest))
172                    Confused(filename, line_number, raw_line)
173
174            # If the enum is scoped, we must prefix enum value with enum name (which is already prefixed
175            # by enclosing classes).
176            if is_enum_class:
177                enum_value = enum_name + '::' + enum_value
178            else:
179                if len(enum_lines.enclosing_classes) > 0:
180                    enum_value = '::'.join(enum_lines.enclosing_classes) + '::' + enum_value
181
182            _ENUMS[enum_name].append((enum_value, enum_text))
183
184
185def main():
186    local_path = sys.argv[1]
187    header_files = []
188    for header_file in sys.argv[2:]:
189        header_files.append(header_file)
190        ProcessFile(header_file)
191
192    print('#include <iostream>')
193    print('')
194
195    for header_file in header_files:
196        header_file = header_file.replace(local_path + '/', '')
197        print('#include "%s"' % header_file)
198
199    print('')
200
201    for enum_name in _ENUMS:
202        print('// This was automatically generated by art/tools/generate_operator_out.py --- do not edit!')
203
204        namespaces = _NAMESPACES[enum_name].split('::')
205        for namespace in namespaces:
206            print('namespace %s {' % namespace)
207
208        print(
209            'std::ostream& operator<<(std::ostream& os, %s rhs) {' % enum_name)
210        print('  switch (rhs) {')
211        for (enum_value, enum_text) in _ENUMS[enum_name]:
212            print('    case %s: os << "%s"; break;' % (enum_value, enum_text))
213        if not _ENUM_CLASSES[enum_name]:
214            print(
215                '    default: os << "%s[" << static_cast<int>(rhs) << "]"; break;' % enum_name)
216        print('  }')
217        print('  return os;')
218        print('}')
219
220        for namespace in reversed(namespaces):
221            print('}  // namespace %s' % namespace)
222        print('')
223
224    sys.exit(0)
225
226
227if __name__ == '__main__':
228    main()
229