summaryrefslogtreecommitdiff
path: root/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator.py
blob: 96fe3a2d0920943a6d2fb1c55a9c50cc4c908026 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# Copyright 2013 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Code shared by the various language-specific code generators."""

from __future__ import print_function

from functools import partial
import os.path
import re

from mojom import fileutil
from mojom.generate import module as mojom
from mojom.generate import pack


def ExpectedArraySize(kind):
  if mojom.IsArrayKind(kind):
    return kind.length
  return None


def SplitCamelCase(identifier):
  """Splits a camel-cased |identifier| and returns a list of lower-cased
  strings.
  """
  # Add underscores after uppercase letters when appropriate. An uppercase
  # letter is considered the end of a word if it is followed by an upper and a
  # lower. E.g. URLLoaderFactory -> URL_LoaderFactory
  identifier = re.sub('([A-Z][0-9]*)(?=[A-Z][0-9]*[a-z])', r'\1_', identifier)
  # Add underscores after lowercase letters when appropriate. A lowercase letter
  # is considered the end of a word if it is followed by an upper.
  # E.g. URLLoaderFactory -> URLLoader_Factory
  identifier = re.sub('([a-z][0-9]*)(?=[A-Z])', r'\1_', identifier)
  return [x.lower() for x in identifier.split('_')]


def ToCamel(identifier, lower_initial=False, digits_split=False, delimiter='_'):
  """Splits |identifier| using |delimiter|, makes the first character of each
  word uppercased (but makes the first character of the first word lowercased
  if |lower_initial| is set to True), and joins the words. Please note that for
  each word, all the characters except the first one are untouched.
  """
  result = ''
  capitalize_next = True
  for i in range(len(identifier)):
    if identifier[i] == delimiter:
      capitalize_next = True
    elif digits_split and identifier[i].isdigit():
      capitalize_next = True
      result += identifier[i]
    elif capitalize_next:
      capitalize_next = False
      result += identifier[i].upper()
    else:
      result += identifier[i]

  if lower_initial and result:
    result = result[0].lower() + result[1:]

  return result


def _ToSnakeCase(identifier, upper=False):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "URL_LOADER_FACTORY" if upper, otherwise "url_loader_factory".
  """
  words = SplitCamelCase(identifier)
  if words[0] == 'k' and len(words) > 1:
    words = words[1:]

  # Variables cannot start with a digit
  if (words[0][0].isdigit()):
    words[0] = '_' + words[0]


  if upper:
    words = map(lambda x: x.upper(), words)

  return '_'.join(words)


def ToUpperSnakeCase(identifier):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "URL_LOADER_FACTORY".
  """
  return _ToSnakeCase(identifier, upper=True)


def ToLowerSnakeCase(identifier):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "url_loader_factory".
  """
  return _ToSnakeCase(identifier, upper=False)


class Stylizer:
  """Stylizers specify naming rules to map mojom names to names in generated
  code. For example, if you would like method_name in mojom to be mapped to
  MethodName in the generated code, you need to define a subclass of Stylizer
  and override StylizeMethod to do the conversion."""

  def StylizeConstant(self, mojom_name):
    return mojom_name

  def StylizeField(self, mojom_name):
    return mojom_name

  def StylizeStruct(self, mojom_name):
    return mojom_name

  def StylizeUnion(self, mojom_name):
    return mojom_name

  def StylizeParameter(self, mojom_name):
    return mojom_name

  def StylizeMethod(self, mojom_name):
    return mojom_name

  def StylizeInterface(self, mojom_name):
    return mojom_name

  def StylizeEnumField(self, mojom_name):
    return mojom_name

  def StylizeEnum(self, mojom_name):
    return mojom_name

  def StylizeFeature(self, mojom_name):
    return mojom_name

  def StylizeModule(self, mojom_namespace):
    return mojom_namespace


def WriteFile(contents, full_path):
  # If |contents| is same with the file content, we skip updating.
  if not isinstance(contents, bytes):
    data = contents.encode('utf8')
  else:
    data = contents

  if os.path.isfile(full_path):
    with open(full_path, 'rb') as destination_file:
      if destination_file.read() == data:
        return

  # Make sure the containing directory exists.
  full_dir = os.path.dirname(full_path)
  fileutil.EnsureDirectoryExists(full_dir)

  # Dump the data to disk.
  with open(full_path, 'wb') as f:
    f.write(data)


def AddComputedData(module):
  """Adds computed data to the given module. The data is computed once and
  used repeatedly in the generation process."""

  def _AddStructComputedData(exported, struct):
    struct.packed = pack.PackedStruct(struct)
    struct.bytes = pack.GetByteLayout(struct.packed)
    struct.versions = pack.GetVersionInfo(struct.packed)
    struct.exported = exported

  def _AddInterfaceComputedData(interface):
    interface.version = 0
    for method in interface.methods:
      # this field is never scrambled
      method.sequential_ordinal = method.ordinal

      if method.min_version is not None:
        interface.version = max(interface.version, method.min_version)

      method.param_struct = _GetStructFromMethod(method)
      if interface.stable:
        method.param_struct.attributes[mojom.ATTRIBUTE_STABLE] = True
        if method.explicit_ordinal is None:
          raise Exception(
              'Stable interfaces must declare explicit method ordinals. The '
              'method %s on stable interface %s does not declare an explicit '
              'ordinal.' % (method.mojom_name, interface.qualified_name))
      interface.version = max(interface.version,
                              method.param_struct.versions[-1].version)

      if method.response_parameters is not None:
        method.response_param_struct = _GetResponseStructFromMethod(method)
        if interface.stable:
          method.response_param_struct.attributes[mojom.ATTRIBUTE_STABLE] = True
        interface.version = max(
            interface.version,
            method.response_param_struct.versions[-1].version)
      else:
        method.response_param_struct = None

  def _GetStructFromMethod(method):
    """Converts a method's parameters into the fields of a struct."""
    params_class = "%s_%s_Params" % (method.interface.mojom_name,
                                     method.mojom_name)
    struct = mojom.Struct(params_class,
                          module=method.interface.module,
                          attributes={})
    for param in method.parameters:
      struct.AddField(
          param.mojom_name,
          param.kind,
          param.ordinal,
          attributes=param.attributes)
    _AddStructComputedData(False, struct)
    return struct

  def _GetResponseStructFromMethod(method):
    """Converts a method's response_parameters into the fields of a struct."""
    params_class = "%s_%s_ResponseParams" % (method.interface.mojom_name,
                                             method.mojom_name)
    struct = mojom.Struct(params_class,
                          module=method.interface.module,
                          attributes={})
    for param in method.response_parameters:
      struct.AddField(
          param.mojom_name,
          param.kind,
          param.ordinal,
          attributes=param.attributes)
    _AddStructComputedData(False, struct)
    return struct

  for struct in module.structs:
    _AddStructComputedData(True, struct)
  for interface in module.interfaces:
    _AddInterfaceComputedData(interface)


class Generator:
  # Pass |output_dir| to emit files to disk. Omit |output_dir| to echo all
  # files to stdout.
  def __init__(self,
               module,
               output_dir=None,
               typemap=None,
               variant=None,
               bytecode_path=None,
               for_blink=False,
               js_generate_struct_deserializers=False,
               export_attribute=None,
               export_header=None,
               generate_non_variant_code=False,
               support_lazy_serialization=False,
               disallow_native_types=False,
               disallow_interfaces=False,
               generate_message_ids=False,
               generate_fuzzing=False,
               enable_kythe_annotations=False,
               extra_cpp_template_paths=None,
               generate_extra_cpp_only=False):
    self.module = module
    self.output_dir = output_dir
    self.typemap = typemap or {}
    self.variant = variant
    self.bytecode_path = bytecode_path
    self.for_blink = for_blink
    self.js_generate_struct_deserializers = js_generate_struct_deserializers
    self.export_attribute = export_attribute
    self.export_header = export_header
    self.generate_non_variant_code = generate_non_variant_code
    self.support_lazy_serialization = support_lazy_serialization
    self.disallow_native_types = disallow_native_types
    self.disallow_interfaces = disallow_interfaces
    self.generate_message_ids = generate_message_ids
    self.generate_fuzzing = generate_fuzzing
    self.enable_kythe_annotations = enable_kythe_annotations
    self.extra_cpp_template_paths = extra_cpp_template_paths
    self.generate_extra_cpp_only = generate_extra_cpp_only

  def Write(self, contents, filename):
    if self.output_dir is None:
      print(contents)
      return
    full_path = os.path.join(self.output_dir, filename)
    WriteFile(contents, full_path)

  def OptimizeEmpty(self, contents):
    # Look for .cc files that contain no actual code. There are many of these
    # and they collectively take a while to compile.
    lines = contents.splitlines()

    for line in lines:
      if line.startswith('#') or line.startswith('//'):
        continue
      if re.match(r'namespace .* {', line) or re.match(r'}.*//.*namespace',
                                                       line):
        continue
      if line.strip():
        # There is some actual code - return the unmodified contents.
        return contents

    # If we reach here then we have a .cc file with no actual code. The
    # includes are therefore unneeded and can be removed.
    new_lines = [line for line in lines if not line.startswith('#include')]
    if len(new_lines) < len(lines):
      new_lines.append('')
      new_lines.append('// Includes removed due to no code being generated.')
    return '\n'.join(new_lines)

  def WriteWithComment(self, contents, filename):
    generator_name = "mojom_bindings_generator.py"
    comment = r"// %s is auto generated by %s, do not edit" % (filename,
                                                               generator_name)
    contents = comment + '\n' + '\n' + contents;
    if filename.endswith('.cc'):
      contents = self.OptimizeEmpty(contents)
    self.Write(contents, filename)

  def GenerateFiles(self, args):
    raise NotImplementedError("Subclasses must override/implement this method")

  def GetJinjaParameters(self):
    """Returns default constructor parameters for the jinja environment."""
    return {}

  def GetGlobals(self):
    """Returns global mappings for the template generation."""
    return {}