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.
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
13 from schema_util
import *
17 from datetime
import datetime
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:
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.
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
42 """Generates a Code object with the schema for the entire namespace.
47 .Append(INFO
% (sys
.argv
[0], self
._namespace
.name
))
49 .Append('/** @fileoverview Externs generated from namespace: %s */' %
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
))
66 def _GenerateType(self
, js_type
):
67 """Given a Type object, returns the Code for this type's definition.
70 if js_type
.property_type
is PropertyType
.ENUM
:
71 c
.Concat(self
._GenerateEnumJsDoc
(js_type
))
73 c
.Concat(self
._GenerateTypeJsDoc
(js_type
))
77 def _GenerateEnumJsDoc(self
, js_type
):
78 """ Given an Enum Type object, returns the Code for the enum's definition.
81 (c
.Sblock(line
='/**', line_prefix
=' * ')
82 .Append('@enum {string}')
83 .Append(self
._GenerateSeeLink
('type', js_type
.simple_name
))
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'.
100 [" %s: '%s'," % (get_property_name(v
.name
), v
.name
)
101 for v
in js_type
.enum_values
]))
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.
118 c
.Sblock(line
='/**', line_prefix
=' * ')
120 if js_type
.description
:
121 for line
in js_type
.description
.splitlines():
124 is_constructor
= self
._IsTypeConstructor
(js_type
)
126 c
.Comment('@constructor', comment_prefix
= ' * ', wrap_indent
=4)
128 c
.Concat(self
._GenerateTypedef
(js_type
.properties
))
130 c
.Append(self
._GenerateSeeLink
('type', js_type
.simple_name
))
133 var
= 'chrome.%s.%s' % (js_type
.namespace
.name
, js_type
.simple_name
)
134 if is_constructor
: var
+= ' = function() {}'
140 def _GenerateTypedef(self
, properties
):
141 """Given an OrderedDict of properties, returns a Code containing a @typedef.
143 if not properties
: return Code()
146 c
.Append('@typedef {')
147 c
.Concat(self
._GenerateObjectDefinition
(properties
), new_line
=False)
148 c
.Append('}', new_line
=False)
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()
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.
165 c
.Append(',', new_line
=False)
167 js_type
= self
._TypeToJsType
(prop
.type_
)
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)
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.
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)
195 c
.Append('=', new_line
=False)
196 c
.Append('} %s' % name
, new_line
=False)
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
)
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
))
222 def _FunctionToJsFunction(self
, function
):
223 """Converts a model.Function to a JS type (i.e., function([params])...)"""
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)
233 c
.Concat(self
._TypeToJsType
(function
.returns
), new_line
=False)
235 c
.Append('void', new_line
=False)
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
:
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)
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.
276 * @param {string} title The new title.
278 chrome.window.setTitle = function(title) {};
281 params
= self
._GenerateFunctionParams
(function
)
282 (c
.Concat(self
._GenerateFunctionJsDoc
(function
))
283 .Append('chrome.%s.%s = function(%s) {};' % (self
._namespace
.name
,
289 def _GenerateEvent(self
, event
):
290 """Generates the code representing an event.
293 /** @type {!ChromeEvent} */
294 chrome.bookmarks.onChildrenReordered;
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
))
303 c
.Append('chrome.%s.%s;' % (self
._namespace
.name
, event
.name
))
306 def _GenerateNamespaceObject(self
):
307 """Generates the code creating namespace object.
313 chrome.bookmarks = {};
319 .Append('chrome.%s = {};' % self
._namespace
.name
))
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
))