Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / json_schema_compiler / js_externs_generator.py
blob20796e9be56895efb7ed5591fac1abd179dee332
1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4 """
5 Generator that produces an externs file for the Closure Compiler.
6 Note: This is a work in progress, and generated externs may require tweaking.
8 See https://developers.google.com/closure/compiler/docs/api-tutorial3#externs
9 """
11 from code import Code
12 from model import *
13 from schema_util import *
15 import os
16 import sys
17 from datetime import datetime
18 import re
20 LICENSE = ("""// Copyright %s The Chromium Authors. All rights reserved.
21 // Use of this source code is governed by a BSD-style license that can be
22 // found in the LICENSE file.
23 """ % datetime.now().year)
25 INFO = """// This file was generated by:
26 // %s.
27 // NOTE: The format of types has changed. 'FooType' is now
28 // 'chrome.%s.FooType'.
29 // Please run the closure compiler before committing changes.
30 // See https://code.google.com/p/chromium/wiki/ClosureCompilation.
31 """
33 class JsExternsGenerator(object):
34 def Generate(self, namespace):
35 return _Generator(namespace).Generate()
37 class _Generator(object):
38 def __init__(self, namespace):
39 self._namespace = namespace
41 def Generate(self):
42 """Generates a Code object with the schema for the entire namespace.
43 """
44 c = Code()
45 (c.Append(LICENSE)
46 .Append()
47 .Append(INFO % (sys.argv[0], self._namespace.name))
48 .Append()
49 .Append('/** @fileoverview Externs generated from namespace: %s */' %
50 self._namespace.name)
51 .Append())
53 c.Cblock(self._GenerateNamespaceObject())
55 for js_type in self._namespace.types.values():
56 c.Cblock(self._GenerateType(js_type))
58 for function in self._namespace.functions.values():
59 c.Cblock(self._GenerateFunction(function))
61 for event in self._namespace.events.values():
62 c.Cblock(self._GenerateEvent(event))
64 return c
66 def _GenerateType(self, js_type):
67 """Given a Type object, returns the Code for this type's definition.
68 """
69 c = Code()
70 if js_type.property_type is PropertyType.ENUM:
71 c.Concat(self._GenerateEnumJsDoc(js_type))
72 else:
73 c.Concat(self._GenerateTypeJsDoc(js_type))
75 return c
77 def _GenerateEnumJsDoc(self, js_type):
78 """ Given an Enum Type object, returns the Code for the enum's definition.
79 """
80 c = Code()
81 (c.Sblock(line='/**', line_prefix=' * ')
82 .Append('@enum {string}')
83 .Append(self._GenerateSeeLink('type', js_type.simple_name))
84 .Eblock(' */'))
85 c.Append('chrome.%s.%s = {' % (self._namespace.name, js_type.name))
87 def get_property_name(e):
88 # Enum properties are normified to be in ALL_CAPS_STYLE.
89 # Assume enum '1ring-rulesThemAll'.
90 # Transform to '1ring-rules_Them_All'.
91 e = re.sub(r'([a-z])([A-Z])', r'\1_\2', e)
92 # Transform to '1ring_rules_Them_All'.
93 e = re.sub(r'\W', '_', e)
94 # Transform to '_1ring_rules_Them_All'.
95 e = re.sub(r'^(\d)', r'_\1', e)
96 # Transform to '_1RING_RULES_THEM_ALL'.
97 return e.upper()
99 c.Append('\n'.join(
100 [" %s: '%s'," % (get_property_name(v.name), v.name)
101 for v in js_type.enum_values]))
102 c.Append('};')
103 return c
105 def _IsTypeConstructor(self, js_type):
106 """Returns true if the given type should be a @constructor. If this returns
107 false, the type is a typedef.
109 return any(prop.type_.property_type is PropertyType.FUNCTION
110 for prop in js_type.properties.values())
112 def _GenerateTypeJsDoc(self, js_type):
113 """Generates the documentation for a type as a Code.
115 Returns an empty code object if the object has no documentation.
117 c = Code()
118 c.Sblock(line='/**', line_prefix=' * ')
120 if js_type.description:
121 for line in js_type.description.splitlines():
122 c.Append(line)
124 is_constructor = self._IsTypeConstructor(js_type)
125 if is_constructor:
126 c.Comment('@constructor', comment_prefix = ' * ', wrap_indent=4)
127 else:
128 c.Concat(self._GenerateTypedef(js_type.properties))
130 c.Append(self._GenerateSeeLink('type', js_type.simple_name))
131 c.Eblock(' */')
133 var = 'chrome.%s.%s' % (js_type.namespace.name, js_type.simple_name)
134 if is_constructor: var += ' = function() {}'
135 var += ';'
136 c.Append(var)
138 return c
140 def _GenerateTypedef(self, properties):
141 """Given an OrderedDict of properties, returns a Code containing a @typedef.
143 if not properties: return Code()
145 c = Code()
146 c.Append('@typedef {')
147 c.Concat(self._GenerateObjectDefinition(properties), new_line=False)
148 c.Append('}', new_line=False)
149 return c
151 def _GenerateObjectDefinition(self, properties):
152 """Given an OrderedDict of properties, returns a Code containing the
153 description of an object.
155 if not properties: return Code()
157 c = Code()
158 c.Sblock('{')
159 first = True
160 for field, prop in properties.items():
161 # Avoid trailing comma.
162 # TODO(devlin): This will be unneeded, if/when
163 # https://github.com/google/closure-compiler/issues/796 is fixed.
164 if not first:
165 c.Append(',', new_line=False)
166 first = False
167 js_type = self._TypeToJsType(prop.type_)
168 if prop.optional:
169 js_type = (Code().
170 Append('(').
171 Concat(js_type, new_line=False).
172 Append('|undefined)', new_line=False))
173 c.Append('%s: ' % field, strip_right=False)
174 c.Concat(js_type, new_line=False)
176 c.Eblock('}')
178 return c
180 def _GenerateFunctionJsDoc(self, function):
181 """Generates the documentation for a function as a Code.
183 Returns an empty code object if the object has no documentation.
185 c = Code()
186 c.Sblock(line='/**', line_prefix=' * ')
188 if function.description:
189 c.Comment(function.description, comment_prefix='')
191 def append_field(c, tag, js_type, name, optional, description):
192 c.Append('@%s {' % tag)
193 c.Concat(js_type, new_line=False)
194 if optional:
195 c.Append('=', new_line=False)
196 c.Append('} %s' % name, new_line=False)
197 if description:
198 c.Comment(' %s' % description, comment_prefix='',
199 wrap_indent=4, new_line=False)
201 for param in function.params:
202 append_field(c, 'param', self._TypeToJsType(param.type_), param.name,
203 param.optional, param.description)
205 if function.callback:
206 append_field(c, 'param', self._FunctionToJsFunction(function.callback),
207 function.callback.name, function.callback.optional,
208 function.callback.description)
210 if function.returns:
211 append_field(c, 'return', self._TypeToJsType(function.returns),
212 '', False, function.returns.description)
214 if function.deprecated:
215 c.Append('@deprecated %s' % function.deprecated)
217 c.Append(self._GenerateSeeLink('method', function.name))
219 c.Eblock(' */')
220 return c
222 def _FunctionToJsFunction(self, function):
223 """Converts a model.Function to a JS type (i.e., function([params])...)"""
224 c = Code()
225 c.Append('function(')
226 for i, param in enumerate(function.params):
227 c.Concat(self._TypeToJsType(param.type_), new_line=False)
228 if i is not len(function.params) - 1:
229 c.Append(', ', new_line=False, strip_right=False)
230 c.Append('):', new_line=False)
232 if function.returns:
233 c.Concat(self._TypeToJsType(function.returns), new_line=False)
234 else:
235 c.Append('void', new_line=False)
237 return c
239 def _TypeToJsType(self, js_type):
240 """Converts a model.Type to a JS type (number, Array, etc.)"""
241 if js_type.property_type in (PropertyType.INTEGER, PropertyType.DOUBLE):
242 return Code().Append('number')
243 if js_type.property_type is PropertyType.OBJECT:
244 if js_type.properties:
245 return self._GenerateObjectDefinition(js_type.properties)
246 return Code().Append('Object')
247 if js_type.property_type is PropertyType.ARRAY:
248 return (Code().Append('!Array<').
249 Concat(self._TypeToJsType(js_type.item_type), new_line=False).
250 Append('>', new_line=False))
251 if js_type.property_type is PropertyType.REF:
252 ref_type = '!chrome.%s.%s' % (self._namespace.name, js_type.ref_type)
253 return Code().Append(ref_type)
254 if js_type.property_type is PropertyType.CHOICES:
255 c = Code()
256 c.Append('(')
257 for i, choice in enumerate(js_type.choices):
258 c.Concat(self._TypeToJsType(choice), new_line=False)
259 if i is not len(js_type.choices) - 1:
260 c.Append('|', new_line=False)
261 c.Append(')', new_line=False)
262 return c
263 if js_type.property_type is PropertyType.FUNCTION:
264 return self._FunctionToJsFunction(js_type.function)
265 if js_type.property_type is PropertyType.ANY:
266 return Code().Append('*')
267 if js_type.property_type.is_fundamental:
268 return Code().Append(js_type.property_type.name)
269 return Code().Append('?') # TODO(tbreisacher): Make this more specific.
271 def _GenerateFunction(self, function):
272 """Generates the code representing a function, including its documentation.
273 For example:
276 * @param {string} title The new title.
278 chrome.window.setTitle = function(title) {};
280 c = Code()
281 params = self._GenerateFunctionParams(function)
282 (c.Concat(self._GenerateFunctionJsDoc(function))
283 .Append('chrome.%s.%s = function(%s) {};' % (self._namespace.name,
284 function.name,
285 params))
287 return c
289 def _GenerateEvent(self, event):
290 """Generates the code representing an event.
291 For example:
293 /** @type {!ChromeEvent} */
294 chrome.bookmarks.onChildrenReordered;
296 c = Code()
297 c.Sblock(line='/**', line_prefix=' * ')
298 if (event.description):
299 c.Comment(event.description, comment_prefix='')
300 c.Append('@type {!ChromeEvent}')
301 c.Append(self._GenerateSeeLink('event', event.name))
302 c.Eblock(' */')
303 c.Append('chrome.%s.%s;' % (self._namespace.name, event.name))
304 return c
306 def _GenerateNamespaceObject(self):
307 """Generates the code creating namespace object.
308 For example:
311 * @const
313 chrome.bookmarks = {};
315 c = Code()
316 (c.Append("""/**
317 * @const
318 */""")
319 .Append('chrome.%s = {};' % self._namespace.name))
320 return c
322 def _GenerateFunctionParams(self, function):
323 params = function.params[:]
324 if function.callback:
325 params.append(function.callback)
326 return ', '.join(param.name for param in params)
328 def _GenerateSeeLink(self, object_type, object_name):
329 """Generates a @see link for a given API 'object' (type, method, or event).
332 # NOTE(devlin): This is kind of a hack. Some APIs will be hosted on
333 # developer.chrome.com/apps/ instead of /extensions/, and some APIs have
334 # '.'s in them (like app.window), which should resolve to 'app_window'.
335 # Luckily, the doc server has excellent url resolution, and knows exactly
336 # what we mean. This saves us from needing any complicated logic here.
337 return ('@see https://developer.chrome.com/extensions/%s#%s-%s' %
338 (self._namespace.name, object_type, object_name))