mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-25 00:58:06 -05:00
it seems that doxygen 1.18.13 is not behaving the same as 1.1811 (Travis one). one might need to remove some spaces in the signature as they are different depending on Doxygen's version
530 lines
19 KiB
Python
530 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
***************************************************************************
|
|
mocked
|
|
---------------------
|
|
Date : May 2017
|
|
Copyright : (C) 2017 by Denis Rouzaud
|
|
Email : denis.rouzaud@gmail.com
|
|
***************************************************************************
|
|
* *
|
|
* 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. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = 'Denis Rouzaud'
|
|
__date__ = 'May 2017'
|
|
__copyright__ = '(C) 2017, Denis Rouzaud'
|
|
|
|
# This will get replaced with a git SHA1 when you do a git archive
|
|
|
|
__revision__ = ':%H$'
|
|
|
|
import re
|
|
import glob
|
|
import os
|
|
|
|
try:
|
|
import xml.etree.cElementTree as ET
|
|
except ImportError:
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
class DoxygenParser():
|
|
|
|
"""
|
|
Parses the XML files generated by Doxygen which describe the API docs
|
|
"""
|
|
|
|
def __init__(self, path, acceptable_missing={}, acceptable_missing_added_note=[], acceptable_missing_brief=[]):
|
|
"""
|
|
Initializes the parser.
|
|
:param path: Path to Doxygen XML output
|
|
"""
|
|
self.acceptable_missing = acceptable_missing
|
|
self.acceptable_missing_added_note = acceptable_missing_added_note
|
|
self.acceptable_missing_brief = acceptable_missing_brief
|
|
self.documentable_members = 0
|
|
self.documented_members = 0
|
|
self.undocumented_members = {}
|
|
self.bindable_members = []
|
|
self.groups = {}
|
|
self.classes_missing_group = []
|
|
self.classes_missing_brief = []
|
|
self.classes_missing_version_added = []
|
|
# for some reason the Doxygen generation on Travis refuses to assign these classes to groups
|
|
self.acceptable_missing_group = ['QgsOgcUtils::LayerProperties',
|
|
'QgsSQLStatement::Node',
|
|
'QgsSQLStatement::NodeBinaryOperator',
|
|
'QgsSQLStatement::NodeColumnRef',
|
|
'QgsSQLStatement::NodeFunction',
|
|
'QgsSQLStatement::NodeInOperator',
|
|
'QgsSQLStatement::NodeList',
|
|
'QgsSQLStatement::NodeLiteral',
|
|
'QgsSQLStatement::NodeUnaryOperator',
|
|
'QgsRuleBasedLabeling::Rule',
|
|
'QgsSQLStatement::Visitor']
|
|
self.version_regex = re.compile(r'QGIS [\d\.]+.*')
|
|
self.parseFiles(path)
|
|
|
|
def parseFiles(self, path):
|
|
""" Parses all the Doxygen XML files in a folder
|
|
:param path: Path to Doxygen XML output
|
|
"""
|
|
|
|
found = False
|
|
# find groups
|
|
for f in glob.glob(os.path.join(path, 'group__*.xml')):
|
|
found = True
|
|
group, members = self.parseGroup(f)
|
|
self.groups[group] = members
|
|
|
|
assert found, "Could not find doxygen groups xml"
|
|
found = False
|
|
# parse docs
|
|
for f in glob.glob(os.path.join(path, '*.xml')):
|
|
found = True
|
|
self.parseFile(f)
|
|
assert found, "Could not find doxygen files xml"
|
|
|
|
def parseGroup(self, f):
|
|
""" Parses a single Doxygen Group XML file
|
|
:param f: XML file path
|
|
"""
|
|
name = None
|
|
members = []
|
|
|
|
# Wrap everything in a try, as sometimes Doxygen XML is malformed
|
|
try:
|
|
for event, elem in ET.iterparse(f):
|
|
if event == 'end' and elem.tag == 'compoundname':
|
|
name = elem.text
|
|
if event == 'end' and elem.tag == 'innerclass':
|
|
members.append(elem.text)
|
|
except:
|
|
pass
|
|
|
|
return name, members
|
|
|
|
def hasGroup(self, class_name):
|
|
""" Returns true if a class has been assigned to a group
|
|
:param class_name class name to test
|
|
"""
|
|
for g in self.groups:
|
|
if class_name in self.groups[g]:
|
|
return True
|
|
return False
|
|
|
|
def parseFile(self, f):
|
|
""" Parses a single Doxygen XML file
|
|
:param f: XML file path
|
|
"""
|
|
documentable_members = 0
|
|
documented_members = 0
|
|
|
|
# Wrap everything in a try, as sometimes Doxygen XML is malformed
|
|
try:
|
|
for event, elem in ET.iterparse(f):
|
|
if event == 'end' and elem.tag == 'compounddef':
|
|
if self.elemIsPublicClass(elem):
|
|
# store documentation status
|
|
members, documented, undocumented, bindable, has_brief_description, found_version_added = self.parseClassElem(elem)
|
|
documentable_members += members
|
|
documented_members += documented
|
|
class_name = elem.find('compoundname').text
|
|
acceptable_missing = self.acceptable_missing.get(class_name, [])
|
|
|
|
if not self.hasGroup(class_name) and class_name not in self.acceptable_missing_group:
|
|
self.classes_missing_group.append(class_name)
|
|
if class_name not in self.acceptable_missing_brief and not has_brief_description:
|
|
self.classes_missing_brief.append(class_name)
|
|
if class_name not in self.acceptable_missing_added_note and not found_version_added:
|
|
self.classes_missing_version_added.append(class_name)
|
|
|
|
# GEN LIST
|
|
# if len(undocumented) > 0:
|
|
# print('"%s": [%s],' % (class_name, ", ".join(['"%s"' % e.replace('"', '\\"') for e in undocumented])))
|
|
|
|
unacceptable_undocumented = undocumented - set(acceptable_missing)
|
|
|
|
# do a case insensitive check too
|
|
unacceptable_undocumented_insensitive = set([u.lower() for u in undocumented]) - set([u.lower() for u in acceptable_missing])
|
|
|
|
if len(unacceptable_undocumented_insensitive) > 0:
|
|
self.undocumented_members[class_name] = {}
|
|
self.undocumented_members[class_name]['documented'] = documented
|
|
self.undocumented_members[class_name]['members'] = members
|
|
self.undocumented_members[class_name]['missing_members'] = unacceptable_undocumented
|
|
|
|
# store bindable members
|
|
if self.classElemIsBindable(elem):
|
|
for m in bindable:
|
|
self.bindable_members.append(m)
|
|
|
|
elem.clear()
|
|
except ET.ParseError as e:
|
|
# sometimes Doxygen generates malformed xml (e.g., for < and > operators)
|
|
line_num, col = e.position
|
|
with open(f, 'r') as xml_file:
|
|
for i, l in enumerate(xml_file):
|
|
if i == line_num - 1:
|
|
line = l
|
|
break
|
|
caret = '{:=>{}}'.format('^', col)
|
|
print(('ParseError in {}\n{}\n{}\n{}'.format(f, e, line, caret)))
|
|
|
|
self.documentable_members += documentable_members
|
|
self.documented_members += documented_members
|
|
|
|
def elemIsPublicClass(self, elem):
|
|
""" Tests whether an XML element corresponds to a public (or protected) class
|
|
:param elem: XML element
|
|
"""
|
|
|
|
# only looking for classes
|
|
if not elem.get('kind') == 'class':
|
|
return False
|
|
|
|
# only looking for public or protected classes
|
|
return elem.get('prot') in ('public', 'protected')
|
|
|
|
def classElemIsBindable(self, elem):
|
|
""" Tests whether a class should have SIP bindings
|
|
:param elem: XML element corresponding to a class
|
|
"""
|
|
try:
|
|
# check for classes with special python doc notes (probably 'not available' or renamed classes, either way
|
|
# they should be safe to ignore as obviously some consideration has been given to Python bindings)
|
|
detailed_sec = elem.find('detaileddescription')
|
|
for p in detailed_sec.getiterator('para'):
|
|
for s in p.getiterator('simplesect'):
|
|
for ps in s.getiterator('para'):
|
|
if ps.text and 'python' in ps.text.lower():
|
|
return False
|
|
return True
|
|
except:
|
|
return True
|
|
|
|
def parseClassElem(self, e):
|
|
""" Parses an XML element corresponding to a Doxygen class
|
|
:param e: XML element
|
|
"""
|
|
documentable_members = 0
|
|
documented_members = 0
|
|
undocumented_members = set()
|
|
bindable_members = []
|
|
# loop through all members
|
|
for m in e.getiterator('memberdef'):
|
|
signature = self.memberSignature(m)
|
|
if signature is None:
|
|
continue
|
|
if self.elemIsBindableMember(m):
|
|
bindable_member = [e.find('compoundname').text, m.find('name').text]
|
|
if bindable_member not in bindable_members:
|
|
bindable_members.append(bindable_member)
|
|
if self.elemIsDocumentableMember(m):
|
|
documentable_members += 1
|
|
if self.memberIsDocumented(m):
|
|
documented_members += 1
|
|
else:
|
|
undocumented_members.add(signature)
|
|
# test for brief description
|
|
d = e.find('briefdescription')
|
|
has_brief_description = False
|
|
if d:
|
|
has_brief_description = True
|
|
|
|
# test for "added in QGIS xxx" string
|
|
d = e.find('detaileddescription')
|
|
found_version_added = False
|
|
for para in d.getiterator('para'):
|
|
for s in para.getiterator('simplesect'):
|
|
if s.get('kind') == 'since':
|
|
for p in s.getiterator('para'):
|
|
if self.version_regex.match(p.text):
|
|
found_version_added = True
|
|
break
|
|
if found_version_added:
|
|
break
|
|
|
|
return documentable_members, documented_members, undocumented_members, bindable_members, has_brief_description, found_version_added
|
|
|
|
def memberSignature(self, elem):
|
|
""" Returns the signature for a member
|
|
:param elem: XML element for a class member
|
|
"""
|
|
a = elem.find('argsstring')
|
|
try:
|
|
if a is not None:
|
|
return elem.find('name').text + a.text
|
|
else:
|
|
return elem.find('name').text
|
|
except:
|
|
return None
|
|
|
|
def elemIsBindableMember(self, elem):
|
|
""" Tests whether an member should be included in SIP bindings
|
|
:param elem: XML element for a class member
|
|
"""
|
|
|
|
# only public or protected members are bindable
|
|
if not self.visibility(elem) in ('public', 'protected'):
|
|
return False
|
|
|
|
# property themselves are not bound, only getters and setters
|
|
if self.isProperty(elem):
|
|
return False
|
|
|
|
# ignore friend classes
|
|
if self.isFriendClass(elem):
|
|
return False
|
|
|
|
# ignore typedefs (can't test for them)
|
|
if self.isTypeDef(elem):
|
|
return False
|
|
|
|
if self.isVariable(elem) and self.visibility(elem) == 'protected':
|
|
# protected variables can't be bound in SIP
|
|
return False
|
|
|
|
# check for members with special python doc notes (probably 'not available' or renamed methods, either way
|
|
# they should be safe to ignore as obviously some consideration has been given to Python bindings)
|
|
try:
|
|
detailed_sec = elem.find('detaileddescription')
|
|
for p in detailed_sec.getiterator('para'):
|
|
for s in p.getiterator('simplesect'):
|
|
for ps in s.getiterator('para'):
|
|
if ps.text and 'python' in ps.text.lower():
|
|
return False
|
|
except:
|
|
pass
|
|
|
|
# ignore constructors and destructor, can't test for these
|
|
if self.isDestructor(elem) or self.isConstructor(elem):
|
|
return False
|
|
|
|
# ignore operators, also can't test
|
|
if self.isOperator(elem):
|
|
return False
|
|
|
|
# ignore deprecated members
|
|
if self.isDeprecated(elem):
|
|
return False
|
|
|
|
return True
|
|
|
|
def elemIsDocumentableMember(self, elem):
|
|
""" Tests whether an member should be included in Doxygen docs
|
|
:param elem: XML element for a class member
|
|
"""
|
|
|
|
# ignore variables (for now, eventually public/protected variables should be documented)
|
|
if self.isVariable(elem):
|
|
return False
|
|
|
|
# only public or protected members should be documented
|
|
if not self.visibility(elem) in ('public', 'protected'):
|
|
return False
|
|
|
|
# ignore reimplemented methods
|
|
if self.isReimplementation(elem):
|
|
return False
|
|
|
|
# ignore friend classes
|
|
if self.isFriendClass(elem):
|
|
return False
|
|
|
|
# ignore destructor
|
|
if self.isDestructor(elem):
|
|
return False
|
|
|
|
# ignore constructors with no arguments
|
|
if self.isConstructor(elem):
|
|
try:
|
|
if elem.find('argsstring').text == '()':
|
|
return False
|
|
except:
|
|
pass
|
|
|
|
name = elem.find('name')
|
|
|
|
# ignore certain obvious operators
|
|
try:
|
|
if name.text in ('operator=', 'operator==', 'operator!='):
|
|
return False
|
|
except:
|
|
pass
|
|
|
|
# ignore on_* slots
|
|
try:
|
|
if name.text.startswith('on_'):
|
|
return False
|
|
except:
|
|
pass
|
|
|
|
# ignore deprecated members
|
|
if self.isDeprecated(elem):
|
|
return False
|
|
|
|
return True
|
|
|
|
def visibility(self, elem):
|
|
""" Returns the visibility of a class or member
|
|
:param elem: XML element for a class or member
|
|
"""
|
|
try:
|
|
return elem.get('prot')
|
|
except:
|
|
return ''
|
|
|
|
def isVariable(self, member_elem):
|
|
""" Tests whether an member is a variable
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
if member_elem.get('kind') == 'variable':
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def isProperty(self, member_elem):
|
|
""" Tests whether an member is a property
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
if member_elem.get('kind') == 'property':
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def isDestructor(self, member_elem):
|
|
""" Tests whether an member is a destructor
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
name = member_elem.find('name').text
|
|
if name.startswith('~'):
|
|
# destructor
|
|
return True
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
def isConstructor(self, member_elem):
|
|
""" Tests whether an member is a constructor
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
definition = member_elem.find('definition').text
|
|
name = member_elem.find('name').text
|
|
if '{}::{}'.format(name, name) in definition:
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def isOperator(self, member_elem):
|
|
""" Tests whether an member is an operator
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
name = member_elem.find('name').text
|
|
if re.match('^operator\W.*', name):
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def isFriendClass(self, member_elem):
|
|
""" Tests whether an member is a friend class
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
if member_elem.get('kind') == 'friend':
|
|
return True
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
def isTypeDef(self, member_elem):
|
|
""" Tests whether an member is a type def
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
try:
|
|
if member_elem.get('kind') == 'typedef':
|
|
return True
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
def isReimplementation(self, member_elem):
|
|
""" Tests whether an member is a reimplementation
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
|
|
# use two different tests, as Doxygen will not detect reimplemented Qt methods
|
|
try:
|
|
if member_elem.find('reimplements') is not None:
|
|
return True
|
|
if ' override' in member_elem.find('argsstring').text:
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def isDeprecated(self, member_elem):
|
|
""" Tests whether an member is deprecated
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
|
|
# look for both Q_DECL_DEPRECATED and Doxygen deprecated tag
|
|
decl_deprecated = False
|
|
type_elem = member_elem.find('type')
|
|
try:
|
|
if 'Q_DECL_DEPRECATED' in type_elem.text:
|
|
decl_deprecated = True
|
|
except:
|
|
pass
|
|
|
|
doxy_deprecated = False
|
|
try:
|
|
for p in member_elem.find('detaileddescription').getiterator('para'):
|
|
for s in p.getiterator('xrefsect'):
|
|
if s.find('xreftitle') is not None and 'Deprecated' in s.find('xreftitle').text:
|
|
doxy_deprecated = True
|
|
break
|
|
except:
|
|
assert 0, member_elem.find('definition').text
|
|
|
|
if not decl_deprecated and not doxy_deprecated:
|
|
return False
|
|
|
|
# only functions for now, but in future this should also apply for enums and variables
|
|
if member_elem.get('kind') in ('function', 'variable'):
|
|
assert decl_deprecated, 'Error: Missing Q_DECL_DEPRECATED for {}'.format(member_elem.find('definition').text)
|
|
assert doxy_deprecated, 'Error: Missing Doxygen deprecated tag for {}'.format(member_elem.find('definition').text)
|
|
|
|
return True
|
|
|
|
def memberIsDocumented(self, member_elem):
|
|
""" Tests whether an member has documentation
|
|
:param member_elem: XML element for a class member
|
|
"""
|
|
for doc_type in ('inbodydescription', 'briefdescription', 'detaileddescription'):
|
|
doc = member_elem.find(doc_type)
|
|
if doc is not None and list(doc):
|
|
return True
|
|
return False
|