1 #! /usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 import itertools 7 import json 8 import os.path 9 import re 10 import sys 11 12 from json_parse import OrderedDict 13 14 # This file is a peer to json_schema.py. Each of these files understands a 15 # certain format describing APIs (either JSON or IDL), reads files written 16 # in that format into memory, and emits them as a Python array of objects 17 # corresponding to those APIs, where the objects are formatted in a way that 18 # the JSON schema compiler understands. compiler.py drives both idl_schema.py 19 # and json_schema.py. 20 21 # idl_parser expects to be able to import certain files in its directory, 22 # so let's set things up the way it wants. 23 _idl_generators_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 24 os.pardir, os.pardir, 'ppapi', 'generators') 25 if _idl_generators_path in sys.path: 26 import idl_parser 27 else: 28 sys.path.insert(0, _idl_generators_path) 29 try: 30 import idl_parser 31 finally: 32 sys.path.pop(0) 33 34 def ProcessComment(comment): 35 ''' 36 Convert a comment into a parent comment and a list of parameter comments. 37 38 Function comments are of the form: 39 Function documentation. May contain HTML and multiple lines. 40 41 |arg1_name|: Description of arg1. Use <var>argument</var> to refer 42 to other arguments. 43 |arg2_name|: Description of arg2... 44 45 Newlines are removed, and leading and trailing whitespace is stripped. 46 47 Args: 48 comment: The string from a Comment node. 49 50 Returns: A tuple that looks like: 51 ( 52 "The processed comment, minus all |parameter| mentions.", 53 { 54 'parameter_name_1': "The comment that followed |parameter_name_1|:", 55 ... 56 } 57 ) 58 ''' 59 def add_paragraphs(content): 60 paragraphs = content.split('\n\n') 61 if len(paragraphs) < 2: 62 return content 63 return '<p>' + '</p><p>'.join(p.strip() for p in paragraphs) + '</p>' 64 65 # Find all the parameter comments of the form '|name|: comment'. 66 parameter_starts = list(re.finditer(r' *\|([^|]*)\| *: *', comment)) 67 68 # Get the parent comment (everything before the first parameter comment. 69 first_parameter_location = (parameter_starts[0].start() 70 if parameter_starts else len(comment)) 71 parent_comment = (add_paragraphs(comment[:first_parameter_location].strip()) 72 .replace('\n', '')) 73 74 params = OrderedDict() 75 for (cur_param, next_param) in itertools.izip_longest(parameter_starts, 76 parameter_starts[1:]): 77 param_name = cur_param.group(1) 78 79 # A parameter's comment goes from the end of its introduction to the 80 # beginning of the next parameter's introduction. 81 param_comment_start = cur_param.end() 82 param_comment_end = next_param.start() if next_param else len(comment) 83 params[param_name] = ( 84 add_paragraphs(comment[param_comment_start:param_comment_end].strip()) 85 .replace('\n', '')) 86 87 return (parent_comment, params) 88 89 90 class Callspec(object): 91 ''' 92 Given a Callspec node representing an IDL function declaration, converts into 93 a tuple: 94 (name, list of function parameters, return type) 95 ''' 96 def __init__(self, callspec_node, comment): 97 self.node = callspec_node 98 self.comment = comment 99 100 def process(self, callbacks): 101 parameters = [] 102 return_type = None 103 if self.node.GetProperty('TYPEREF') not in ('void', None): 104 return_type = Typeref(self.node.GetProperty('TYPEREF'), 105 self.node.parent, 106 {'name': self.node.GetName()}).process(callbacks) 107 # The IDL parser doesn't allow specifying return types as optional. 108 # Instead we infer any object return values to be optional. 109 # TODO(asargent): fix the IDL parser to support optional return types. 110 if return_type.get('type') == 'object' or '$ref' in return_type: 111 return_type['optional'] = True 112 for node in self.node.GetChildren(): 113 parameter = Param(node).process(callbacks) 114 if parameter['name'] in self.comment: 115 parameter['description'] = self.comment[parameter['name']] 116 parameters.append(parameter) 117 return (self.node.GetName(), parameters, return_type) 118 119 120 class Param(object): 121 ''' 122 Given a Param node representing a function parameter, converts into a Python 123 dictionary that the JSON schema compiler expects to see. 124 ''' 125 def __init__(self, param_node): 126 self.node = param_node 127 128 def process(self, callbacks): 129 return Typeref(self.node.GetProperty('TYPEREF'), 130 self.node, 131 {'name': self.node.GetName()}).process(callbacks) 132 133 134 class Dictionary(object): 135 ''' 136 Given an IDL Dictionary node, converts into a Python dictionary that the JSON 137 schema compiler expects to see. 138 ''' 139 def __init__(self, dictionary_node): 140 self.node = dictionary_node 141 142 def process(self, callbacks): 143 properties = OrderedDict() 144 for node in self.node.GetChildren(): 145 if node.cls == 'Member': 146 k, v = Member(node).process(callbacks) 147 properties[k] = v 148 result = {'id': self.node.GetName(), 149 'properties': properties, 150 'type': 'object'} 151 if self.node.GetProperty('nodoc'): 152 result['nodoc'] = True 153 elif self.node.GetProperty('inline_doc'): 154 result['inline_doc'] = True 155 elif self.node.GetProperty('noinline_doc'): 156 result['noinline_doc'] = True 157 return result 158 159 160 161 class Member(object): 162 ''' 163 Given an IDL dictionary or interface member, converts into a name/value pair 164 where the value is a Python dictionary that the JSON schema compiler expects 165 to see. 166 ''' 167 def __init__(self, member_node): 168 self.node = member_node 169 170 def process(self, callbacks): 171 properties = OrderedDict() 172 name = self.node.GetName() 173 if self.node.GetProperty('deprecated'): 174 properties['deprecated'] = self.node.GetProperty('deprecated') 175 if self.node.GetProperty('allowAmbiguousOptionalArguments'): 176 properties['allowAmbiguousOptionalArguments'] = True 177 for property_name in ('OPTIONAL', 'nodoc', 'nocompile', 'nodart'): 178 if self.node.GetProperty(property_name): 179 properties[property_name.lower()] = True 180 for option_name, sanitizer in [ 181 ('maxListeners', int), 182 ('supportsFilters', lambda s: s == 'true'), 183 ('supportsListeners', lambda s: s == 'true'), 184 ('supportsRules', lambda s: s == 'true')]: 185 if self.node.GetProperty(option_name): 186 if 'options' not in properties: 187 properties['options'] = {} 188 properties['options'][option_name] = sanitizer(self.node.GetProperty( 189 option_name)) 190 is_function = False 191 parameter_comments = OrderedDict() 192 for node in self.node.GetChildren(): 193 if node.cls == 'Comment': 194 (parent_comment, parameter_comments) = ProcessComment(node.GetName()) 195 properties['description'] = parent_comment 196 elif node.cls == 'Callspec': 197 is_function = True 198 name, parameters, return_type = (Callspec(node, parameter_comments) 199 .process(callbacks)) 200 properties['parameters'] = parameters 201 if return_type is not None: 202 properties['returns'] = return_type 203 properties['name'] = name 204 if is_function: 205 properties['type'] = 'function' 206 else: 207 properties = Typeref(self.node.GetProperty('TYPEREF'), 208 self.node, properties).process(callbacks) 209 enum_values = self.node.GetProperty('legalValues') 210 if enum_values: 211 if properties['type'] == 'integer': 212 enum_values = map(int, enum_values) 213 elif properties['type'] == 'double': 214 enum_values = map(float, enum_values) 215 properties['enum'] = enum_values 216 return name, properties 217 218 219 class Typeref(object): 220 ''' 221 Given a TYPEREF property representing the type of dictionary member or 222 function parameter, converts into a Python dictionary that the JSON schema 223 compiler expects to see. 224 ''' 225 def __init__(self, typeref, parent, additional_properties): 226 self.typeref = typeref 227 self.parent = parent 228 self.additional_properties = additional_properties 229 230 def process(self, callbacks): 231 properties = self.additional_properties 232 result = properties 233 234 if self.parent.GetPropertyLocal('OPTIONAL'): 235 properties['optional'] = True 236 237 # The IDL parser denotes array types by adding a child 'Array' node onto 238 # the Param node in the Callspec. 239 for sibling in self.parent.GetChildren(): 240 if sibling.cls == 'Array' and sibling.GetName() == self.parent.GetName(): 241 properties['type'] = 'array' 242 properties['items'] = OrderedDict() 243 properties = properties['items'] 244 break 245 246 if self.typeref == 'DOMString': 247 properties['type'] = 'string' 248 elif self.typeref == 'boolean': 249 properties['type'] = 'boolean' 250 elif self.typeref == 'double': 251 properties['type'] = 'number' 252 elif self.typeref == 'long': 253 properties['type'] = 'integer' 254 elif self.typeref == 'any': 255 properties['type'] = 'any' 256 elif self.typeref == 'object': 257 properties['type'] = 'object' 258 if 'additionalProperties' not in properties: 259 properties['additionalProperties'] = OrderedDict() 260 properties['additionalProperties']['type'] = 'any' 261 instance_of = self.parent.GetProperty('instanceOf') 262 if instance_of: 263 properties['isInstanceOf'] = instance_of 264 elif self.typeref == 'ArrayBuffer': 265 properties['type'] = 'binary' 266 properties['isInstanceOf'] = 'ArrayBuffer' 267 elif self.typeref == 'FileEntry': 268 properties['type'] = 'object' 269 properties['isInstanceOf'] = 'FileEntry' 270 if 'additionalProperties' not in properties: 271 properties['additionalProperties'] = OrderedDict() 272 properties['additionalProperties']['type'] = 'any' 273 elif self.parent.GetPropertyLocal('Union'): 274 choices = [] 275 properties['choices'] = [Typeref(node.GetProperty('TYPEREF'), 276 node, 277 OrderedDict()).process(callbacks) 278 for node in self.parent.GetChildren() 279 if node.cls == 'Option'] 280 elif self.typeref is None: 281 properties['type'] = 'function' 282 else: 283 if self.typeref in callbacks: 284 # Do not override name and description if they are already specified. 285 name = properties.get('name', None) 286 description = properties.get('description', None) 287 properties.update(callbacks[self.typeref]) 288 if description is not None: 289 properties['description'] = description 290 if name is not None: 291 properties['name'] = name 292 else: 293 properties['$ref'] = self.typeref 294 return result 295 296 297 class Enum(object): 298 ''' 299 Given an IDL Enum node, converts into a Python dictionary that the JSON 300 schema compiler expects to see. 301 ''' 302 def __init__(self, enum_node): 303 self.node = enum_node 304 self.description = '' 305 306 def process(self, callbacks): 307 enum = [] 308 for node in self.node.GetChildren(): 309 if node.cls == 'EnumItem': 310 enum_value = {'name': node.GetName()} 311 for child in node.GetChildren(): 312 if child.cls == 'Comment': 313 enum_value['description'] = ProcessComment(child.GetName())[0] 314 else: 315 raise ValueError('Did not process %s %s' % (child.cls, child)) 316 enum.append(enum_value) 317 elif node.cls == 'Comment': 318 self.description = ProcessComment(node.GetName())[0] 319 else: 320 sys.exit('Did not process %s %s' % (node.cls, node)) 321 result = {'id' : self.node.GetName(), 322 'description': self.description, 323 'type': 'string', 324 'enum': enum} 325 for property_name in ( 326 'inline_doc', 'noinline_doc', 'nodoc', 'cpp_enum_prefix_override',): 327 if self.node.GetProperty(property_name): 328 result[property_name] = self.node.GetProperty(property_name) 329 if self.node.GetProperty('deprecated'): 330 result[deprecated] = self.node.GetProperty('deprecated') 331 return result 332 333 334 class Namespace(object): 335 ''' 336 Given an IDLNode representing an IDL namespace, converts into a Python 337 dictionary that the JSON schema compiler expects to see. 338 ''' 339 340 def __init__(self, 341 namespace_node, 342 description, 343 nodoc=False, 344 internal=False, 345 platforms=None, 346 compiler_options=None, 347 deprecated=None): 348 self.namespace = namespace_node 349 self.nodoc = nodoc 350 self.internal = internal 351 self.platforms = platforms 352 self.compiler_options = compiler_options 353 self.events = [] 354 self.functions = [] 355 self.types = [] 356 self.callbacks = OrderedDict() 357 self.description = description 358 self.deprecated = deprecated 359 360 def process(self): 361 for node in self.namespace.GetChildren(): 362 if node.cls == 'Dictionary': 363 self.types.append(Dictionary(node).process(self.callbacks)) 364 elif node.cls == 'Callback': 365 k, v = Member(node).process(self.callbacks) 366 self.callbacks[k] = v 367 elif node.cls == 'Interface' and node.GetName() == 'Functions': 368 self.functions = self.process_interface(node) 369 elif node.cls == 'Interface' and node.GetName() == 'Events': 370 self.events = self.process_interface(node) 371 elif node.cls == 'Enum': 372 self.types.append(Enum(node).process(self.callbacks)) 373 else: 374 sys.exit('Did not process %s %s' % (node.cls, node)) 375 if self.compiler_options is not None: 376 compiler_options = self.compiler_options 377 else: 378 compiler_options = {} 379 return {'namespace': self.namespace.GetName(), 380 'description': self.description, 381 'nodoc': self.nodoc, 382 'types': self.types, 383 'functions': self.functions, 384 'internal': self.internal, 385 'events': self.events, 386 'platforms': self.platforms, 387 'compiler_options': compiler_options, 388 'deprecated': self.deprecated} 389 390 def process_interface(self, node): 391 members = [] 392 for member in node.GetChildren(): 393 if member.cls == 'Member': 394 name, properties = Member(member).process(self.callbacks) 395 members.append(properties) 396 return members 397 398 399 class IDLSchema(object): 400 ''' 401 Given a list of IDLNodes and IDLAttributes, converts into a Python list 402 of api_defs that the JSON schema compiler expects to see. 403 ''' 404 405 def __init__(self, idl): 406 self.idl = idl 407 408 def process(self): 409 namespaces = [] 410 nodoc = False 411 internal = False 412 description = None 413 platforms = None 414 compiler_options = {} 415 deprecated = None 416 for node in self.idl: 417 if node.cls == 'Namespace': 418 if not description: 419 # TODO(kalman): Go back to throwing an error here. 420 print('%s must have a namespace-level comment. This will ' 421 'appear on the API summary page.' % node.GetName()) 422 description = '' 423 namespace = Namespace(node, description, nodoc, internal, 424 platforms=platforms, 425 compiler_options=compiler_options or None, 426 deprecated=deprecated) 427 namespaces.append(namespace.process()) 428 nodoc = False 429 internal = False 430 platforms = None 431 compiler_options = None 432 elif node.cls == 'Copyright': 433 continue 434 elif node.cls == 'Comment': 435 description = node.GetName() 436 elif node.cls == 'ExtAttribute': 437 if node.name == 'nodoc': 438 nodoc = bool(node.value) 439 elif node.name == 'internal': 440 internal = bool(node.value) 441 elif node.name == 'platforms': 442 platforms = list(node.value) 443 elif node.name == 'implemented_in': 444 compiler_options['implemented_in'] = node.value 445 elif node.name == 'camel_case_enum_to_string': 446 compiler_options['camel_case_enum_to_string'] = node.value 447 elif node.name == 'deprecated': 448 deprecated = str(node.value) 449 else: 450 continue 451 else: 452 sys.exit('Did not process %s %s' % (node.cls, node)) 453 return namespaces 454 455 456 def Load(filename): 457 ''' 458 Given the filename of an IDL file, parses it and returns an equivalent 459 Python dictionary in a format that the JSON schema compiler expects to see. 460 ''' 461 462 f = open(filename, 'r') 463 contents = f.read() 464 f.close() 465 466 idl = idl_parser.IDLParser().ParseData(contents, filename) 467 idl_schema = IDLSchema(idl) 468 return idl_schema.process() 469 470 471 def Main(): 472 ''' 473 Dump a json serialization of parse result for the IDL files whose names 474 were passed in on the command line. 475 ''' 476 if len(sys.argv) > 1: 477 for filename in sys.argv[1:]: 478 schema = Load(filename) 479 print json.dumps(schema, indent=2) 480 else: 481 contents = sys.stdin.read() 482 idl = idl_parser.IDLParser().ParseData(contents, '<stdin>') 483 schema = IDLSchema(idl).process() 484 print json.dumps(schema, indent=2) 485 486 487 if __name__ == '__main__': 488 Main() 489