genapixml.py 29.4 KB
Newer Older
1 2
#!/usr/bin/python

Ghislain MARY's avatar
Ghislain MARY committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright (C) 2014 Belledonne Communications SARL
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
Ghislain MARY's avatar
Ghislain MARY committed
18

19
import argparse
20
import logging
21
import os
22
import six
23 24 25 26
import string
import sys
import xml.etree.ElementTree as ET
import xml.dom.minidom as minidom
27

28
import metadoc
29 30


31 32 33
logger = logging.getLogger(__name__)


34 35 36 37 38 39
class CObject:
	def __init__(self, name):
		self.name = name.strip()
		self.briefDescription = ''
		self.detailedDescription = None
		self.deprecated = False
40
		self.briefDoc = None
41
		self.detailedDoc = None
42 43 44


class CEnumValue(CObject):
45 46 47
	def __init__(self, name):
		CObject.__init__(self, name)
		self.value = None
48 49 50 51 52 53 54


class CEnum(CObject):
	def __init__(self, name):
		CObject.__init__(self, name)
		self.values = []
		self.associatedTypedef = None
55 56 57 58
	
	@property
	def publicName(self):
		return self.associatedTypedef.name if self.associatedTypedef is not None else self.name
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

	def addValue(self, value):
		self.values.append(value)


class CStructMember(CObject):
	def __init__(self, name, t):
		CObject.__init__(self, name)
		self.ctype = t.strip()


class CStruct(CObject):
	def __init__(self, name):
		CObject.__init__(self, name)
		self.members = []
		self.associatedTypedef = None

	def addMember(self, member):
		self.members.append(member)


class CTypedef(CObject):
	def __init__(self, name, definition):
		CObject.__init__(self, name)
		self.definition = definition.strip()


class CArgument(CObject):
	def __init__(self, t, name = '', enums = [], structs = []):
		CObject.__init__(self, name)
		self.description = None
90
		self.containedType = None
91
		keywords = [ 'const', 'struct', 'enum', 'signed', 'unsigned', 'short', 'long', '*' ]
92 93 94 95 96 97 98 99 100 101 102 103
		fullySplittedType = []
		splittedType = t.strip().split(' ')
		for s in splittedType:
			if s.startswith('*'):
				fullySplittedType.append('*')
				if len(s) > 1:
					fullySplittedType.append(s[1:])
			elif s.endswith('*'):
				fullySplittedType.append(s[:-1])
				fullySplittedType.append('*')
			else:
				fullySplittedType.append(s)
104 105
		if 'MS2_DEPRECATED' in fullySplittedType:
			fullySplittedType.remove('MS2_DEPRECATED')
Ghislain MARY's avatar
Ghislain MARY committed
106 107
		elif 'LINPHONE_DEPRECATED' in fullySplittedType:
			fullySplittedType.remove('LINPHONE_DEPRECATED')
108 109
		isStruct = False
		isEnum = False
110
		self.ctype = 'int' # Default to int so that the result is correct eg. for 'unsigned short'
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
		for s in fullySplittedType:
			if not s in keywords:
				self.ctype = s
			if s == 'struct':
				isStruct = True
			if s == 'enum':
				isEnum = True
		if isStruct:
			for st in structs:
				if st.associatedTypedef is not None:
					self.ctype = st.associatedTypedef.name
		elif isEnum:
			for e in enums:
				if e.associatedTypedef is not None:
					self.ctype = e.associatedTypedef.name
126 127 128 129 130 131
		if self.ctype == 'int' and 'int' not in fullySplittedType:
			if fullySplittedType[-1] == '*':
				fullySplittedType.insert(-1, 'int')
			else:
				fullySplittedType.append('int')
		self.completeType = ' '.join(fullySplittedType)
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

	def __str__(self):
		return self.completeType + " " + self.name


class CArgumentsList:
	def __init__(self):
		self.arguments = []

	def addArgument(self, arg):
		self.arguments.append(arg)

	def __len__(self):
		return len(self.arguments)

	def __getitem__(self, key):
		return self.arguments[key]

	def __str__(self):
		argstr = []
		for arg in self.arguments:
			argstr.append(str(arg))
		return ', '.join(argstr)


class CFunction(CObject):
	def __init__(self, name, returnarg, argslist):
		CObject.__init__(self, name)
		self.returnArgument = returnarg
		self.arguments = argslist
		self.location = None


class CEvent(CFunction):
	pass


class CProperty:
	def __init__(self, name):
		self.name = name
		self.getter = None
		self.setter = None


class CClass(CObject):
	def __init__(self, st):
		CObject.__init__(self, st.associatedTypedef.name)
		if st.deprecated or st.associatedTypedef.deprecated:
			self.deprecated = True
		if len(st.associatedTypedef.briefDescription) > 0:
			self.briefDescription = st.associatedTypedef.briefDescription
		elif len(st.briefDescription) > 0:
			self.briefDescription = st.briefDescription
		if st.associatedTypedef.detailedDescription is not None:
			self.detailedDescription = st.associatedTypedef.detailedDescription
		elif st.detailedDescription is not None:
			self.detailedDescription = st.detailedDescription
		self.__struct = st
		self.events = {}
		self.classMethods = {}
		self.instanceMethods = {}
		self.properties = {}
		self.__computeCFunctionPrefix()

	def __computeCFunctionPrefix(self):
		self.cFunctionPrefix = ''
		first = True
		for l in self.name:
			if l.isupper() and not first:
				self.cFunctionPrefix += '_'
			self.cFunctionPrefix += l.lower()
			first = False
		self.cFunctionPrefix += '_'

	def __addPropertyGetter(self, name, f):
		if not name in self.properties:
			prop = CProperty(name)
			self.properties[name] = prop
		self.properties[name].getter = f

	def __addPropertySetter(self, name, f):
		if not name in self.properties:
			prop = CProperty(name)
			self.properties[name] = prop
		self.properties[name].setter = f

	def __addClassMethod(self, f):
219 220
		if not f.name in self.classMethods:
			self.classMethods[f.name] = f
221 222 223

	def __addInstanceMethod(self, f):
		name = f.name[len(self.cFunctionPrefix):]
224
		if name.startswith('get_') and len(f.arguments) == 1:
225
			self.__addPropertyGetter(name[4:], f)
226
		elif name.startswith('is_') and len(f.arguments) == 1 and f.returnArgument.ctype == 'bool_t':
227
			self.__addPropertyGetter(name, f)
228
		elif name.endswith('_enabled') and len(f.arguments) == 1 and f.returnArgument.ctype == 'bool_t':
229
			self.__addPropertyGetter(name, f)
230
		elif name.startswith('set_') and len(f.arguments) == 2:
231
			self.__addPropertySetter(name[4:], f)
232
		elif name.startswith('enable_') and len(f.arguments) == 2 and f.arguments[1].ctype == 'bool_t':
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
			self.__addPropertySetter(name[7:] + '_enabled', f)
		else:
			if not f.name in self.instanceMethods:
				self.instanceMethods[f.name] = f

	def addEvent(self, ev):
		if not ev.name in self.events:
			self.events[ev.name] = ev

	def addMethod(self, f):
		if len(f.arguments) > 0 and f.arguments[0].ctype == self.name:
			self.__addInstanceMethod(f)
		else:
			self.__addClassMethod(f)


class Project:
	def __init__(self):
		self.prettyPrint = False
		self.enums = []
		self.__structs = []
		self.__typedefs = []
		self.__events = []
		self.__functions = []
		self.classes = []
258
		self.docparser = metadoc.Parser()
259 260 261

	def add(self, elem):
		if isinstance(elem, CClass):
262
			logger.debug("Adding class " + elem.name)
263 264
			self.classes.append(elem)
		elif isinstance(elem, CEnum):
265 266 267 268
			msg = 'Adding enum {0}'.format(elem.name)
			for value in elem.values:
				msg += ('\t{0}'.format(value))
			logger.debug(msg)
269 270
			self.enums.append(elem)
		elif isinstance(elem, CStruct):
271 272 273 274
			msg = "Adding struct " + elem.name
			for sm in elem.members:
				msg += ('\t{0} {1}'.format(sm.ctype, sm.name))
			logger.debug(msg)
275 276
			self.__structs.append(elem)
		elif isinstance(elem, CTypedef):
277
			logger.debug('Adding typedef {0}\t{1}'.format(elem.name, elem.definition))
278 279
			self.__typedefs.append(elem)
		elif isinstance(elem, CEvent):
280
			logger.debug('Adding event {0}\tReturns: {1}\tArguments: {2}'.format(elem.name, elem.returnArgument.ctype, elem.arguments))
281 282
			self.__events.append(elem)
		elif isinstance(elem, CFunction):
283
			logger.debug('Adding event {0}\tReturns: {1}\tArguments: {2}'.format(elem.name, elem.returnArgument.ctype, elem.arguments))
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
			self.__functions.append(elem)

	def __cleanDescription(self, descriptionNode):
		for para in descriptionNode.findall('./para'):
			for n in para.findall('./parameterlist'):
				para.remove(n)
			for n in para.findall("./simplesect[@kind='return']"):
				para.remove(n)
			for n in para.findall("./simplesect[@kind='see']"):
				t = ''.join(n.itertext())
				n.clear()
				n.tag = 'see'
				n.text = t
			for n in para.findall("./simplesect[@kind='note']"):
				n.tag = 'note'
				n.attrib = {}
			for n in para.findall(".//xrefsect"):
				para.remove(n)
			for n in para.findall('.//ref'):
				n.attrib = {}
304
			for n in para.findall(".//bctbx_list"):
305
				para.remove(n)
306 307 308 309 310 311
		if descriptionNode.tag == 'parameterdescription':
			descriptionNode.tag = 'description'
		if descriptionNode.tag == 'simplesect':
			descriptionNode.tag = 'description'
			descriptionNode.attrib = {}
		return descriptionNode
312 313 314
	
	def __canBeWrapped(self, node):
		return node.find('./detaileddescription//donotwrap') is None
315 316 317

	def __discoverClasses(self):
		for td in self.__typedefs:
318
			if td.definition.startswith('enum '):
319 320 321 322
				for e in self.enums:
					if (e.associatedTypedef is None) and td.definition[5:] == e.name:
						e.associatedTypedef = td
						break
323
			elif td.definition.startswith('struct '):
324 325 326 327 328 329 330 331
				structFound = False
				for st in self.__structs:
					if (st.associatedTypedef is None) and td.definition[7:] == st.name:
						st.associatedTypedef = td
						structFound = True
						break
				if not structFound:
					name = td.definition[7:]
332
					logger.warning("Structure with no associated typedef: " + name)
333 334 335 336
					st = CStruct(name)
					st.associatedTypedef = td
					self.add(st)
		for td in self.__typedefs:
337
			if td.definition.startswith('struct '):
338 339
				for st in self.__structs:
					if st.associatedTypedef == td:
340 341
						cclass = CClass(st)
						cclass.briefDoc = td.briefDoc
342
						cclass.detailedDoc = td.detailedDoc
343
						self.add(cclass)
344
						break
345 346 347
			elif ('Linphone' + td.definition) == td.name:
				st = CStruct(td.name)
				st.associatedTypedef = td
348 349
				cclass = CClass(st)
				cclass.briefDoc = td.briefDoc
350
				cclass.detailedDoc = td.detailedDoc
351
				self.add(st)
352
				self.add(cclass)
353 354 355
		# Sort classes by length of name (longest first), so that methods are put in the right class
		self.classes.sort(key = lambda c: len(c.name), reverse = True)
		for e in self.__events:
356
			eventAdded = False
357
			for c in self.classes:
358
				if c.name.endswith('Cbs') and e.name.startswith(c.name):
359
					c.addEvent(e)
360 361 362 363 364 365 366 367
					eventAdded = True
					break
			if not eventAdded:
				for c in self.classes:
					if e.name.startswith(c.name):
						c.addEvent(e)
						eventAdded = True
						break
368 369 370 371 372 373
		for f in self.__functions:
			for c in self.classes:
				if c.cFunctionPrefix == f.name[0 : len(c.cFunctionPrefix)]:
					c.addMethod(f)
					break

374 375 376 377 378 379 380 381 382
	def __parseCEnumValueInitializer(self, initializer):
		initializer = initializer.strip()
		if not initializer.startswith('='):
			return None

		initializer = initializer[1:]
		initializer.strip()
		return initializer

383 384
	def __parseCEnumValue(self, node):
		ev = CEnumValue(node.find('./name').text)
385 386 387 388
		initializerNode = node.find('./initializer')
		if initializerNode is not None:
			ev.value = self.__parseCEnumValueInitializer(initializerNode.text)

389 390 391 392
		deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
		if deprecatedNode is not None:
			ev.deprecated = True
		ev.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
393
		ev.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
394
		ev.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
395 396 397 398
		ev.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
		return ev

	def __parseCEnumMemberdef(self, node):
399 400
		if not Project.__canBeWrapped(self, node):
			return None
401 402 403 404 405
		e = CEnum(node.find('./name').text)
		deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
		if deprecatedNode is not None:
			e.deprecated = True
		e.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
406
		e.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
407
		e.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
		e.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
		enumvalues = node.findall("enumvalue[@prot='public']")
		for enumvalue in enumvalues:
			ev = self.__parseCEnumValue(enumvalue)
			e.addValue(ev)
		return e

	def __findCEnum(self, tree):
		memberdefs = tree.findall("./compounddef[@kind='group']/sectiondef[@kind='enum']/memberdef[@kind='enum'][@prot='public']")
		for m in memberdefs:
			e = self.__parseCEnumMemberdef(m)
			self.add(e)

	def __parseCStructMember(self, node, structname):
		name = node.find('./name').text
		definition = node.find('./definition').text
424
		t = definition[0:definition.find(structname + "::" + name)]
425 426 427 428 429
		sm = CStructMember(name, t)
		deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
		if deprecatedNode is not None:
			sm.deprecated = True
		sm.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
430
		sm.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
431
		sm.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
432 433 434 435 436 437 438 439 440
		sm.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
		return sm

	def __parseCStructCompounddef(self, node):
		s = CStruct(node.find('./compoundname').text)
		deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
		if deprecatedNode is not None:
			s.deprecated = True
		s.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
441
		s.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
442
		s.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
443 444 445 446 447 448 449 450 451 452 453 454 455 456
		s.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
		structmembers = node.findall("sectiondef/memberdef[@kind='variable'][@prot='public']")
		for structmember in structmembers:
			sm = self.__parseCStructMember(structmember, s.name)
			s.addMember(sm)
		return s

	def __findCStruct(self, tree):
		compounddefs = tree.findall("./compounddef[@kind='struct'][@prot='public']")
		for c in compounddefs:
			s = self.__parseCStructCompounddef(c)
			self.add(s)

	def __parseCTypedefMemberdef(self, node):
457 458
		if not Project.__canBeWrapped(self, node):
			return None
459 460
		name = node.find('./name').text
		definition = node.find('./definition').text
461
		if definition.startswith('typedef '):
462
			definition = definition[8 :]
463
		if name.endswith('Cb'):
464
			pos = definition.find("(*")
465 466 467 468
			if pos == -1:
				return None
			returntype = definition[0:pos].strip()
			returnarg = CArgument(returntype, enums = self.enums, structs = self.__structs)
469 470
			returndesc = node.find("./detaileddescription/para/simplesect[@kind='return']")
			if returndesc is not None:
471
				if returnarg.ctype == 'MSList' or returnarg.ctype == 'bctbx_list_t':
472
					n = returndesc.find('.//bctbxlist')
473 474 475 476 477
					if n is not None:
						returnarg.containedType = n.text
				returnarg.description = self.__cleanDescription(returndesc)
			elif returnarg.completeType != 'void':
				missingDocWarning += "\tReturn value is not documented\n"
478
			definition = definition[pos + 2 :]
479
			pos = definition.find("(")
480 481 482 483
			definition = definition[pos + 1 : -1]
			argslist = CArgumentsList()
			for argdef in definition.split(', '):
				argType = ''
484 485
				starPos = argdef.rfind('*')
				spacePos = argdef.rfind(' ')
486 487 488 489 490 491
				if starPos != -1:
					argType = argdef[0 : starPos + 1]
					argName = argdef[starPos + 1 :]
				elif spacePos != -1:
					argType = argdef[0 : spacePos]
					argName = argdef[spacePos + 1 :]
492 493 494 495 496 497 498 499
				arg = CArgument(argType, argName, self.enums, self.__structs)
				if arg.ctype == 'MSList' or arg.ctype == 'bctbx_list_t':
					for argentry in node.findall("detaileddescription/para/parameterlist[@kind='param']/*"):
						if argentry.find("parameternamelist[parametername='{0}']".format(argName)) is not None:
							containedType = argentry.find("parameterdescription//bctbxlist")
							arg.containedType = containedType.text if containedType is not None else None
							break
				argslist.addArgument(arg)
500 501 502 503 504 505 506 507 508 509 510 511
			if len(argslist) > 0:
				paramdescs = node.findall("detaileddescription/para/parameterlist[@kind='param']/parameteritem")
				if paramdescs:
					for arg in argslist.arguments:
						for paramdesc in paramdescs:
							if arg.name == paramdesc.find('./parameternamelist').find('./parametername').text:
								arg.description = self.__cleanDescription(paramdesc.find('./parameterdescription'))
					missingDocWarning = ''
					for arg in argslist.arguments:
						if arg.description == None:
							missingDocWarning += "\t'" + arg.name + "' parameter not documented\n";
					if missingDocWarning != '':
512
						logger.warning(name + ":\n" + missingDocWarning)
513 514 515 516 517
			f = CEvent(name, returnarg, argslist)
			deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
			if deprecatedNode is not None:
				f.deprecated = True
			f.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
518
			f.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
519
			f.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
520 521 522
			f.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
			return f
		else:
523
			pos = definition.rfind(" " + name)
524 525 526 527 528 529 530
			if pos != -1:
				definition = definition[0 : pos]
			td = CTypedef(name, definition)
			deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
			if deprecatedNode is not None:
				td.deprecated = True
			td.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
531
			td.briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
532
			td.detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
533 534 535 536 537 538 539 540 541 542 543
			td.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
			return td
		return None

	def __findCTypedef(self, tree):
		memberdefs = tree.findall("./compounddef[@kind='group']/sectiondef[@kind='typedef']/memberdef[@kind='typedef'][@prot='public']")
		for m in memberdefs:
			td = self.__parseCTypedefMemberdef(m)
			self.add(td)

	def __parseCFunctionMemberdef(self, node):
544 545
		if not Project.__canBeWrapped(self, node):
			return None
546 547 548
		internal = node.find("./detaileddescription/internal")
		if internal is not None:
			return None
549 550 551 552 553
		
		# The doc must be parsed here since the XML tree is to be modified in below code
		briefDoc = self.docparser.parse_description(node.find('./briefdescription'))
		detailedDoc = self.docparser.parse_description(node.find('./detaileddescription'))
		
554 555 556 557 558 559
		missingDocWarning = ''
		name = node.find('./name').text
		t = ''.join(node.find('./type').itertext())
		returnarg = CArgument(t, enums = self.enums, structs = self.__structs)
		returndesc = node.find("./detaileddescription/para/simplesect[@kind='return']")
		if returndesc is not None:
560
			if returnarg.ctype == 'MSList' or returnarg.ctype == 'bctbx_list_t':
561
				n = returndesc.find('.//bctbxlist')
562 563
				if n is not None:
					returnarg.containedType = n.text
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
			returnarg.description = self.__cleanDescription(returndesc)
		elif returnarg.completeType != 'void':
			missingDocWarning += "\tReturn value is not documented\n"
		argslist = CArgumentsList()
		argslistNode = node.findall('./param')
		for argNode in argslistNode:
			argType = ''.join(argNode.find('./type').itertext())
			argName = ''
			argNameNode = argNode.find('./declname')
			if argNameNode is not None:
				argName = ''.join(argNameNode.itertext())
			if argType != 'void':
				argslist.addArgument(CArgument(argType, argName, self.enums, self.__structs))
		if len(argslist) > 0:
			paramdescs = node.findall("./detaileddescription/para/parameterlist[@kind='param']/parameteritem")
			if paramdescs:
				for arg in argslist.arguments:
					for paramdesc in paramdescs:
						if arg.name == paramdesc.find('./parameternamelist').find('./parametername').text:
583
							if arg.ctype == 'MSList' or arg.ctype == 'bctbx_list_t':
584
								n = paramdesc.find('.//bctbxlist')
585 586
								if n is not None:
									arg.containedType = n.text
587 588 589 590 591 592 593 594 595 596 597
							arg.description = self.__cleanDescription(paramdesc.find('./parameterdescription'))
				missingDocWarning = ''
				for arg in argslist.arguments:
					if arg.description == None:
						missingDocWarning += "\t'" + arg.name + "' parameter not documented\n";
		f = CFunction(name, returnarg, argslist)
		deprecatedNode = node.find(".//xrefsect[xreftitle='Deprecated']")
		if deprecatedNode is not None:
			f.deprecated = True
		f.briefDescription = ''.join(node.find('./briefdescription').itertext()).strip()
		f.detailedDescription = self.__cleanDescription(node.find('./detaileddescription'))
598 599
		if f.briefDescription == '' and ''.join(f.detailedDescription.itertext()).strip() == '':
			return None
600 601
		f.briefDoc = briefDoc
		f.detailedDoc = detailedDoc
602 603 604 605 606 607
		locationNode = node.find('./location')
		if locationNode is not None:
			f.location = locationNode.get('file')
			if not f.location.endswith('.h'):
				missingDocWarning += "\tNot documented in a header file ('" + f.location + "')\n";
		if missingDocWarning != '':
608
			logger.warning(name + ":\n" + missingDocWarning)
609 610 611 612 613 614
		return f

	def __findCFunction(self, tree):
		memberdefs = tree.findall("./compounddef[@kind='group']/sectiondef[@kind='func']/memberdef[@kind='function'][@prot='public'][@static='no']")
		for m in memberdefs:
			f = self.__parseCFunctionMemberdef(m)
615 616
			if f is not None:
				self.add(f)
617 618 619 620 621 622

	def initFromFiles(self, xmlfiles):
		trees = []
		for f in xmlfiles:
			tree = None
			try:
623
				logger.debug("Parsing XML file: " + f)
624 625
				tree = ET.parse(f)
			except ET.ParseError as e:
626
				logger.error(e)
627 628 629 630 631 632 633 634 635 636 637 638
			if tree is not None:
				trees.append(tree)
		for tree in trees:
			self.__findCEnum(tree)
		for tree in trees:
			self.__findCStruct(tree)
		for tree in trees:
			self.__findCTypedef(tree)
		for tree in trees:
			self.__findCFunction(tree)
		self.__discoverClasses()

639 640 641 642
	def initFromDir(self, xmldir):
		files = [ os.path.join(xmldir, f) for f in os.listdir(xmldir) if (os.path.isfile(os.path.join(xmldir, f)) and f.endswith('.xml')) ]
		self.initFromFiles(files)

643 644
	def check(self):
		for c in self.classes:
645
			for name, p in six.iteritems(c.properties):
646
				if p.getter is None and p.setter is not None:
647
					logger.warning("Property '" + name + "' of class '" + c.name + "' has a setter but no getter")
648 649 650 651 652 653 654


class Generator:
	def __init__(self, outputfile):
		self.__outputfile = outputfile

	def __generateEnum(self, cenum, enumsNode):
655
		enumNodeAttributes = { 'name' : cenum.name, 'deprecated' : str(cenum.deprecated).lower() }
656 657 658 659 660 661 662 663 664 665
		if cenum.associatedTypedef is not None:
			enumNodeAttributes['name'] = cenum.associatedTypedef.name
		enumNode = ET.SubElement(enumsNode, 'enum', enumNodeAttributes)
		if cenum.briefDescription != '':
			enumBriefDescriptionNode = ET.SubElement(enumNode, 'briefdescription')
			enumBriefDescriptionNode.text = cenum.briefDescription
		enumNode.append(cenum.detailedDescription)
		if len(cenum.values) > 0:
			enumValuesNode = ET.SubElement(enumNode, 'values')
			for value in cenum.values:
666
				enumValuesNodeAttributes = { 'name' : value.name, 'deprecated' : str(value.deprecated).lower() }
667 668 669 670 671 672 673
				valueNode = ET.SubElement(enumValuesNode, 'value', enumValuesNodeAttributes)
				if value.briefDescription != '':
					valueBriefDescriptionNode = ET.SubElement(valueNode, 'briefdescription')
					valueBriefDescriptionNode.text = value.briefDescription
				valueNode.append(value.detailedDescription)

	def __generateFunction(self, parentNode, nodeName, f):
674
		functionAttributes = { 'name' : f.name, 'deprecated' : str(f.deprecated).lower() }
675 676 677
		if f.location is not None:
			functionAttributes['location'] = f.location
		functionNode = ET.SubElement(parentNode, nodeName, functionAttributes)
678
		returnValueAttributes = { 'type' : f.returnArgument.ctype, 'completetype' : f.returnArgument.completeType }
679 680
		if f.returnArgument.containedType is not None:
			returnValueAttributes['containedtype'] = f.returnArgument.containedType
681 682 683 684 685
		returnValueNode = ET.SubElement(functionNode, 'return', returnValueAttributes)
		if f.returnArgument.description is not None:
			returnValueNode.append(f.returnArgument.description)
		argumentsNode = ET.SubElement(functionNode, 'arguments')
		for arg in f.arguments:
686
			argumentNodeAttributes = { 'name' : arg.name, 'type' : arg.ctype, 'completetype' : arg.completeType }
687 688
			if arg.containedType is not None:
				argumentNodeAttributes['containedtype'] = arg.containedType
689 690 691 692 693 694 695 696 697
			argumentNode = ET.SubElement(argumentsNode, 'argument', argumentNodeAttributes)
			if arg.description is not None:
				argumentNode.append(arg.description)
		if f.briefDescription != '':
			functionBriefDescriptionNode = ET.SubElement(functionNode, 'briefdescription')
			functionBriefDescriptionNode.text = f.briefDescription
		functionNode.append(f.detailedDescription)

	def __generateClass(self, cclass, classesNode):
698 699 700 701 702
		# Do not include classes that contain nothing
		if len(cclass.events) == 0 and len(cclass.classMethods) == 0 and \
			len(cclass.instanceMethods) == 0 and len(cclass.properties) == 0:
			return
		# Check the capabilities of the class
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
		has_ref_method = False
		has_unref_method = False
		has_destroy_method = False
		for methodname in cclass.instanceMethods:
			methodname_without_prefix = methodname.replace(cclass.cFunctionPrefix, '')
			if methodname_without_prefix == 'ref':
				has_ref_method = True
			elif methodname_without_prefix == 'unref':
				has_unref_method = True
			elif methodname_without_prefix == 'destroy':
				has_destroy_method = True
		refcountable = False
		destroyable = False
		if has_ref_method and has_unref_method:
			refcountable = True
		if has_destroy_method:
			destroyable = True
		classNodeAttributes = {
			'name' : cclass.name,
			'cfunctionprefix' : cclass.cFunctionPrefix,
			'deprecated' : str(cclass.deprecated).lower(),
			'refcountable' : str(refcountable).lower(),
			'destroyable' : str(destroyable).lower()
		}
727
		# Generate the XML node for the class
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771
		classNode = ET.SubElement(classesNode, 'class', classNodeAttributes)
		if len(cclass.events) > 0:
			eventsNode = ET.SubElement(classNode, 'events')
			eventnames = []
			for eventname in cclass.events:
				eventnames.append(eventname)
			eventnames.sort()
			for eventname in eventnames:
				self.__generateFunction(eventsNode, 'event', cclass.events[eventname])
		if len(cclass.classMethods) > 0:
			classMethodsNode = ET.SubElement(classNode, 'classmethods')
			methodnames = []
			for methodname in cclass.classMethods:
				methodnames.append(methodname)
			methodnames.sort()
			for methodname in methodnames:
				self.__generateFunction(classMethodsNode, 'classmethod', cclass.classMethods[methodname])
		if len(cclass.instanceMethods) > 0:
			instanceMethodsNode = ET.SubElement(classNode, 'instancemethods')
			methodnames = []
			for methodname in cclass.instanceMethods:
				methodnames.append(methodname)
			methodnames.sort()
			for methodname in methodnames:
				self.__generateFunction(instanceMethodsNode, 'instancemethod', cclass.instanceMethods[methodname])
		if len(cclass.properties) > 0:
			propertiesNode = ET.SubElement(classNode, 'properties')
			propnames = []
			for propname in cclass.properties:
				propnames.append(propname)
			propnames.sort()
			for propname in propnames:
				propertyNodeAttributes = { 'name' : propname }
				propertyNode = ET.SubElement(propertiesNode, 'property', propertyNodeAttributes)
				if cclass.properties[propname].getter is not None:
					self.__generateFunction(propertyNode, 'getter', cclass.properties[propname].getter)
				if cclass.properties[propname].setter is not None:
					self.__generateFunction(propertyNode, 'setter', cclass.properties[propname].setter)
		if cclass.briefDescription != '':
			classBriefDescriptionNode = ET.SubElement(classNode, 'briefdescription')
			classBriefDescriptionNode.text = cclass.briefDescription
		classNode.append(cclass.detailedDescription)

	def generate(self, project):
772
		logger.info("Generating XML document of Linphone API to '" + self.__outputfile.name + "'")
773 774 775 776 777 778 779 780 781 782 783
		apiNode = ET.Element('api')
		project.enums.sort(key = lambda e: e.name)
		if len(project.enums) > 0:
			enumsNode = ET.SubElement(apiNode, 'enums')
			for cenum in project.enums:
				self.__generateEnum(cenum, enumsNode)
		if len(project.classes) > 0:
			classesNode = ET.SubElement(apiNode, 'classes')
			project.classes.sort(key = lambda c: c.name)
			for cclass in project.classes:
				self.__generateClass(cclass, classesNode)
784
		s = '<?xml version="1.0" encoding="UTF-8" ?>\n'.encode('utf-8')
785 786 787 788 789 790 791 792 793 794 795 796 797 798
		s += ET.tostring(apiNode, 'utf-8')
		if project.prettyPrint:
			s = minidom.parseString(s).toprettyxml(indent='\t')
		self.__outputfile.write(s)



def main(argv = None):
	if argv is None:
		argv = sys.argv
	argparser = argparse.ArgumentParser(description="Generate XML version of the Linphone API.")
	argparser.add_argument('-o', '--outputfile', metavar='outputfile', type=argparse.FileType('w'), help="Output XML file describing the Linphone API.")
	argparser.add_argument('--verbose', help="Increase output verbosity", action='store_true')
	argparser.add_argument('--pretty', help="XML pretty print", action='store_true')
799
	argparser.add_argument('xmldir', help="XML directory generated by doxygen.")
800 801 802 803 804
	args = argparser.parse_args()
	if args.outputfile == None:
		args.outputfile = open('api.xml', 'w')
	project = Project()
	if args.verbose:
805 806 807
		logger.setLogLevel(logging.DEBUG)
	else:
		logger.setLogLevel(logging.INFO)
808 809
	if args.pretty:
		project.prettyPrint = True
810
	project.initFromDir(args.xmldir)
811 812 813 814 815 816
	project.check()
	gen = Generator(args.outputfile)
	gen.generate(project)

if __name__ == "__main__":
	sys.exit(main())