Merge pull request #1715 from jakobandersen/cpp-enum-support

C++, enum support and fix of HTML search.
This commit is contained in:
Georg Brandl 2015-02-09 07:32:24 +01:00
commit 5d30675e5b
4 changed files with 303 additions and 37 deletions

View File

@ -13,6 +13,8 @@ Features added
* The ``language`` config value is now available in the HTML templates. * The ``language`` config value is now available in the HTML templates.
* The ``env-updated`` event can now return a value, which is interpreted * The ``env-updated`` event can now return a value, which is interpreted
as an iterable of additional docnames that need to be rewritten. as an iterable of additional docnames that need to be rewritten.
* #772: Support for scoped and unscoped enums in C++. Enumerators in unscoped
enums are injected into the parent scope in addition to the enum scope.
* Add ``todo_include_todos`` config option to quickstart conf file, handled as * Add ``todo_include_todos`` config option to quickstart conf file, handled as
described in documentation. described in documentation.
@ -27,6 +29,7 @@ Bugs fixed
* #1687: linkcheck now treats 401 Unauthorized responses as "working". * #1687: linkcheck now treats 401 Unauthorized responses as "working".
* #1690: toctrees with ``glob`` option now can also contain entries for single * #1690: toctrees with ``glob`` option now can also contain entries for single
documents with explicit title. documents with explicit title.
* #1591: html search results for C++ elements now has correct interpage links.
Release 1.3b2 (released Dec 5, 2014) Release 1.3b2 (released Dec 5, 2014)

View File

@ -520,8 +520,8 @@ The C++ Domain
The C++ domain (name **cpp**) supports documenting C++ projects. The C++ domain (name **cpp**) supports documenting C++ projects.
The following directives are available. All declarations can start with a visibility statement The following directives are available. All declarations can start with
(``public``, ``private`` or ``protected``). a visibility statement (``public``, ``private`` or ``protected``).
.. rst:directive:: .. cpp:class:: class speicifer .. rst:directive:: .. cpp:class:: class speicifer
@ -578,29 +578,63 @@ The following directives are available. All declarations can start with a visibi
Declaration of a type alias with unspecified type. Declaration of a type alias with unspecified type.
.. rst:directive:: .. cpp:enum:: unscoped enum declaration
.. cpp:enum-struct:: scoped enum declaration
.. cpp:enum-class:: scoped enum declaration
Describe a (scoped) enum, possibly with the underlying type specified.
Any enumerators declared inside an unscoped enum will be declared both in the enum scope
and in the parent scope.
Examples:
.. cpp:enum:: MyEnum
An unscoped enum.
.. cpp:enum:: MySpecificEnum : long
An unscoped enum with specified underlying type.
.. cpp:enum-class:: MyScopedEnum
A scoped enum.
.. cpp:enum-struct:: protected MyScopedVisibilityEnum : std::underlying_type<MySpecificEnum>::type
A scoped enum with non-default visibility, and with a specified underlying type.
.. rst:directive:: .. cpp:enumerator:: name
.. cpp:enumerator:: name = constant
Describe an enumerator, optionally with its value defined.
.. rst:directive:: .. cpp:namespace:: namespace .. rst:directive:: .. cpp:namespace:: namespace
Select the current namespace for the following objects. Note that the namespace Select the current namespace for the subsequent objects. Note that the namespace
does not need to correspond to C++ namespaces, but can end in names of classes, e.g.,:: does not need to correspond to C++ namespaces, but can end in names of classes, e.g.,::
.. cpp:namespace:: Namespace1::Namespace2::SomeClass::AnInnerClass .. cpp:namespace:: Namespace1::Namespace2::SomeClass::AnInnerClass
All following objects will be defined as if their name were declared with the namespace All subsequent objects will be defined as if their name were declared with the namespace
prepended. The following cross-references will be search for by both their specified name prepended. The subsequent cross-references will be searched for by both their specified name
and with the namespace prepended. and with the namespace prepended.
Using ``NULL``, ``0``, or ``nullptr`` as the namespace will reset it to the global namespace.
.. _cpp-roles: .. _cpp-roles:
These roles link to the given object types: These roles link to the given object types:
.. rst:role:: cpp:class .. rst:role:: cpp:class
cpp:func cpp:func
cpp:member cpp:member
cpp:type cpp:type
cpp:enum
cpp:enumerator
Reference a C++ object. You can give the full specification (and need to, for Reference a C++ object by name. The name must be properly qualified relative to the
overloaded functions.) position of the link.
.. note:: .. note::

View File

@ -121,6 +121,15 @@
# (note: only "0" is allowed as the value, according to the standard, # (note: only "0" is allowed as the value, according to the standard,
# right?) # right?)
enum-head ->
enum-key attribute-specifier-seq[opt] nested-name-specifier[opt]
identifier enum-base[opt]
enum-key -> "enum" | "enum struct" | "enum class"
enum-base ->
":" type
enumerator-definition ->
identifier
| identifier "=" constant-expression
We additionally add the possibility for specifying the visibility as the We additionally add the possibility for specifying the visibility as the
first thing. first thing.
@ -139,7 +148,6 @@
grammar, typedef-like: no initilizer grammar, typedef-like: no initilizer
decl-specifier-seq declerator decl-specifier-seq declerator
member_object: member_object:
goal: as a type_object which must have a declerator, and optionally goal: as a type_object which must have a declerator, and optionally
with a initializer with a initializer
@ -150,6 +158,30 @@
goal: a function declaration, TODO: what about templates? for now: skip goal: a function declaration, TODO: what about templates? for now: skip
grammar: no initializer grammar: no initializer
decl-specifier-seq declerator decl-specifier-seq declerator
class_object:
goal: a class declaration, but with specification of a base class
TODO: what about templates? for now: skip
grammar:
nested-name
| nested-name ":"
'comma-separated list of nested-name optionally with visibility'
enum_object:
goal: an unscoped enum or a scoped enum, optionally with the underlying
type specified
grammar:
("class" | "struct")[opt] visibility[opt] nested-name (":" type)[opt]
enumerator_object:
goal: an element in a scoped or unscoped enum. The name should be
injected according to the scopedness.
grammar:
nested-name ("=" constant-expression)
namespace_object:
goal: a directive to put all following declarations in a specific scope
grammar:
nested-name
""" """
import re import re
@ -1300,12 +1332,13 @@ class ASTBaseClass(ASTBase):
signode += addnodes.desc_annotation( signode += addnodes.desc_annotation(
self.visibility, self.visibility) self.visibility, self.visibility)
signode += nodes.Text(' ') signode += nodes.Text(' ')
self.name.describe_signature(signode, mode, env) self.name.describe_signature(signode, mode, env)
class ASTClass(ASTBase): class ASTClass(ASTBase):
def __init__(self, name, bases): def __init__(self, name, visibility, bases):
self.name = name self.name = name
self.visibility = visibility
self.bases = bases self.bases = bases
def get_id_v1(self): def get_id_v1(self):
@ -1320,6 +1353,9 @@ class ASTClass(ASTBase):
def __unicode__(self): def __unicode__(self):
res = [] res = []
if self.visibility != 'public':
res.append(self.visibility)
res.append(' ')
res.append(text_type(self.name)) res.append(text_type(self.name))
if len(self.bases) > 0: if len(self.bases) > 0:
res.append(' : ') res.append(' : ')
@ -1333,6 +1369,10 @@ class ASTClass(ASTBase):
def describe_signature(self, signode, mode, env): def describe_signature(self, signode, mode, env):
_verify_description_mode(mode) _verify_description_mode(mode)
if self.visibility != 'public':
signode += addnodes.desc_annotation(
self.visibility, self.visibility)
signode += nodes.Text(' ')
self.name.describe_signature(signode, mode, env) self.name.describe_signature(signode, mode, env)
if len(self.bases) > 0: if len(self.bases) > 0:
signode += nodes.Text(' : ') signode += nodes.Text(' : ')
@ -1341,6 +1381,69 @@ class ASTClass(ASTBase):
signode += nodes.Text(', ') signode += nodes.Text(', ')
signode.pop() signode.pop()
class ASTEnum(ASTBase):
def __init__(self, name, visibility, scoped, underlyingType):
self.name = name
self.visibility = visibility
self.scoped = scoped
self.underlyingType = underlyingType
def get_id_v1(self):
return None # did not exist at that time
def get_id_v2(self):
return _id_prefix_v2 + self.prefixedName.get_id_v2()
def __unicode__(self):
res = []
if self.scoped:
res.append(self.scoped)
res.append(' ')
if self.visibility != 'public':
res.append(self.visibility)
res.append(' ')
res.append(text_type(self.name))
if self.underlyingType:
res.append(' : ')
res.append(text_type(self.underlyingType))
return u''.join(res)
def describe_signature(self, signode, mode, env):
_verify_description_mode(mode)
# self.scoped has been done by the CPPEnumObject
if self.visibility != 'public':
signode += addnodes.desc_annotation(
self.visibility, self.visibility)
signode += nodes.Text(' ')
self.name.describe_signature(signode, mode, env)
if self.underlyingType:
signode += nodes.Text(' : ')
self.underlyingType.describe_signature(signode, 'noneIsName', env)
class ASTEnumerator(ASTBase):
def __init__(self, name, init):
self.name = name
self.init = init
def get_id_v1(self):
return None # did not exist at that time
def get_id_v2(self):
return _id_prefix_v2 + self.prefixedName.get_id_v2()
def __unicode__(self):
res = []
res.append(text_type(self.name))
if self.init:
res.append(text_type(self.init))
return u''.join(res)
def describe_signature(self, signode, mode, env):
_verify_description_mode(mode)
self.name.describe_signature(signode, mode, env)
if self.init:
self.init.describe_signature(signode, 'noneIsName')
class DefinitionParser(object): class DefinitionParser(object):
# those without signedness and size modifiers # those without signedness and size modifiers
@ -1891,6 +1994,9 @@ class DefinitionParser(object):
return ASTTypeWithInit(type, init) return ASTTypeWithInit(type, init)
def _parse_class(self): def _parse_class(self):
classVisibility = 'public'
if self.match(_visibility_re):
classVisibility = self.matched_text
name = self._parse_nested_name() name = self._parse_nested_name()
bases = [] bases = []
self.skip_ws() self.skip_ws()
@ -1907,7 +2013,30 @@ class DefinitionParser(object):
continue continue
else: else:
break break
return ASTClass(name, bases) return ASTClass(name, classVisibility, bases)
def _parse_enum(self):
scoped = None # is set by CPPEnumObject
self.skip_ws()
visibility = 'public'
if self.match(_visibility_re):
visibility = self.matched_text
self.skip_ws()
name = self._parse_nested_name()
self.skip_ws()
underlyingType = None
if self.skip_string(':'):
underlyingType = self._parse_type()
return ASTEnum(name, visibility, scoped, underlyingType)
def _parse_enumerator(self):
name = self._parse_nested_name()
self.skip_ws()
init = None
if self.skip_string('='):
self.skip_ws()
init = ASTInitializer(self.read_rest())
return ASTEnumerator(name, init)
def parse_type_object(self): def parse_type_object(self):
res = self._parse_type(outer='type') res = self._parse_type(outer='type')
@ -1929,6 +2058,16 @@ class DefinitionParser(object):
res.objectType = 'class' res.objectType = 'class'
return res return res
def parse_enum_object(self):
res = self._parse_enum()
res.objectType = 'enum'
return res
def parse_enumerator_object(self):
res = self._parse_enumerator()
res.objectType = 'enumerator'
return res
def parse_namespace_object(self): def parse_namespace_object(self):
res = self._parse_nested_name() res = self._parse_nested_name()
res.objectType = 'namespace' res.objectType = 'namespace'
@ -1960,6 +2099,8 @@ class CPPObject(ObjectDescription):
ast.get_id_v1() ast.get_id_v1()
] ]
theid = ids[0] theid = ids[0]
ast.newestId = theid
assert theid # shouldn't be None
name = text_type(ast.prefixedName) name = text_type(ast.prefixedName)
if theid not in self.state.document.ids: if theid not in self.state.document.ids:
# if the name is not unique, the first one will win # if the name is not unique, the first one will win
@ -1970,18 +2111,35 @@ class CPPObject(ObjectDescription):
pass pass
#print("[CPP] non-unique name:", name) #print("[CPP] non-unique name:", name)
for id in ids: for id in ids:
signode['ids'].append(id) if id: # is None when the element didn't exist in that version
signode['ids'].append(id)
signode['first'] = (not self.names) signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
if name not in objects: if not name in objects:
objects.setdefault(name, objects.setdefault(name, (self.env.docname, ast))
(self.env.docname, ast.objectType, theid)) if ast.objectType == 'enumerator':
# find the parent, if it exists && is an enum
# && it's unscoped,
# then add the name to the parent scope
assert len(ast.prefixedName.names) > 0
parentPrefixedAstName = ASTNestedName(ast.prefixedName.names[:-1])
parentPrefixedName = text_type(parentPrefixedAstName)
if parentPrefixedName in objects:
docname, parentAst = objects[parentPrefixedName]
if parentAst.objectType == 'enum' and not parentAst.scoped:
enumeratorName = ASTNestedName([ast.prefixedName.names[-1]])
assert len(parentAst.prefixedName.names) > 0
enumScope = ASTNestedName(parentAst.prefixedName.names[:-1])
unscopedName = enumeratorName.prefix_nested_name(enumScope)
txtUnscopedName = text_type(unscopedName)
if not txtUnscopedName in objects:
objects.setdefault(txtUnscopedName,
(self.env.docname, ast))
# add the uninstantiated template if it doesn't exist # add the uninstantiated template if it doesn't exist
uninstantiated = ast.prefixedName.get_name_no_last_template() uninstantiated = ast.prefixedName.get_name_no_last_template()
if uninstantiated != name and uninstantiated not in objects: if uninstantiated != name and uninstantiated not in objects:
signode['names'].append(uninstantiated) signode['names'].append(uninstantiated)
objects.setdefault(uninstantiated, ( objects.setdefault(uninstantiated, (self.env.docname, ast))
self.env.docname, ast.objectType, theid))
self.env.ref_context['cpp:lastname'] = ast.prefixedName self.env.ref_context['cpp:lastname'] = ast.prefixedName
indextext = self.get_index_text(name) indextext = self.get_index_text(name)
@ -2057,12 +2215,14 @@ class CPPClassObject(CPPObject):
return _('%s (C++ class)') % name return _('%s (C++ class)') % name
def before_content(self): def before_content(self):
lastname = self.env.ref_context['cpp:lastname'] # lastname may not be set if there was an error
assert lastname if 'cpp:lastname' in self.env.ref_context:
if 'cpp:parent' in self.env.ref_context: lastname = self.env.ref_context['cpp:lastname']
self.env.ref_context['cpp:parent'].append(lastname) assert lastname
else: if 'cpp:parent' in self.env.ref_context:
self.env.ref_context['cpp:parent'] = [lastname] self.env.ref_context['cpp:parent'].append(lastname)
else:
self.env.ref_context['cpp:parent'] = [lastname]
def after_content(self): def after_content(self):
self.env.ref_context['cpp:parent'].pop() self.env.ref_context['cpp:parent'].pop()
@ -2075,6 +2235,57 @@ class CPPClassObject(CPPObject):
ast.describe_signature(signode, 'lastIsName', self.env) ast.describe_signature(signode, 'lastIsName', self.env)
class CPPEnumObject(CPPObject):
def get_index_text(self, name):
return _('%s (C++ enum)') % name
def before_content(self):
# lastname may not be set if there was an error
if 'cpp:lastname' in self.env.ref_context:
lastname = self.env.ref_context['cpp:lastname']
assert lastname
if 'cpp:parent' in self.env.ref_context:
self.env.ref_context['cpp:parent'].append(lastname)
else:
self.env.ref_context['cpp:parent'] = [lastname]
def after_content(self):
self.env.ref_context['cpp:parent'].pop()
def parse_definition(self, parser):
ast = parser.parse_enum_object()
# self.objtype is set by ObjectDescription in run()
if self.objtype == "enum":
ast.scoped = None
elif self.objtype == "enum-struct":
ast.scoped = "struct"
elif self.objtype == "enum-class":
ast.scoped = "class"
else:
assert False
return ast
def describe_signature(self, signode, ast):
prefix = 'enum '
if ast.scoped:
prefix += ast.scoped
prefix += ' '
signode += addnodes.desc_annotation(prefix, prefix)
ast.describe_signature(signode, 'lastIsName', self.env)
class CPPEnumeratorObject(CPPObject):
def get_index_text(self, name):
return _('%s (C++ enumerator)') % name
def parse_definition(self, parser):
return parser.parse_enumerator_object()
def describe_signature(self, signode, ast):
signode += addnodes.desc_annotation('enumerator ', 'enumerator ')
ast.describe_signature(signode, 'lastIsName', self.env)
class CPPNamespaceObject(Directive): class CPPNamespaceObject(Directive):
""" """
This directive is just to tell Sphinx that we're documenting stuff in This directive is just to tell Sphinx that we're documenting stuff in
@ -2120,7 +2331,6 @@ class CPPXRefRole(XRefRole):
title = title[dcolon + 2:] title = title[dcolon + 2:]
return title, target return title, target
class CPPDomain(Domain): class CPPDomain(Domain):
"""C++ language domain.""" """C++ language domain."""
name = 'cpp' name = 'cpp'
@ -2129,7 +2339,9 @@ class CPPDomain(Domain):
'class': ObjType(l_('class'), 'class'), 'class': ObjType(l_('class'), 'class'),
'function': ObjType(l_('function'), 'func'), 'function': ObjType(l_('function'), 'func'),
'member': ObjType(l_('member'), 'member'), 'member': ObjType(l_('member'), 'member'),
'type': ObjType(l_('type'), 'type') 'type': ObjType(l_('type'), 'type'),
'enum': ObjType(l_('enum'), 'enum'),
'enumerator': ObjType(l_('enumerator'), 'enumerator')
} }
directives = { directives = {
@ -2137,16 +2349,22 @@ class CPPDomain(Domain):
'function': CPPFunctionObject, 'function': CPPFunctionObject,
'member': CPPMemberObject, 'member': CPPMemberObject,
'type': CPPTypeObject, 'type': CPPTypeObject,
'enum': CPPEnumObject,
'enum-struct': CPPEnumObject,
'enum-class': CPPEnumObject,
'enumerator': CPPEnumeratorObject,
'namespace': CPPNamespaceObject 'namespace': CPPNamespaceObject
} }
roles = { roles = {
'class': CPPXRefRole(), 'class': CPPXRefRole(),
'func': CPPXRefRole(fix_parens=True), 'func': CPPXRefRole(fix_parens=True),
'member': CPPXRefRole(), 'member': CPPXRefRole(),
'type': CPPXRefRole() 'type': CPPXRefRole(),
'enum': CPPXRefRole(),
'enumerator': CPPXRefRole()
} }
initial_data = { initial_data = {
'objects': {}, # prefixedName -> (docname, objectType, id) 'objects': {}, # prefixedName -> (docname, ast)
} }
def clear_doc(self, docname): def clear_doc(self, docname):
@ -2169,9 +2387,9 @@ class CPPDomain(Domain):
name = nameAst.get_name_no_last_template() name = nameAst.get_name_no_last_template()
if name not in self.data['objects']: if name not in self.data['objects']:
return None, None return None, None
docname, objectType, id = self.data['objects'][name] docname, ast = self.data['objects'][name]
return make_refnode(builder, fromdocname, docname, id, contnode, return make_refnode(builder, fromdocname, docname, ast.newestId,
name), objectType contnode, name), ast.objectType
parser = DefinitionParser(target) parser = DefinitionParser(target)
try: try:
@ -2210,5 +2428,5 @@ class CPPDomain(Domain):
return [] return []
def get_objects(self): def get_objects(self):
for refname, (docname, type, theid) in iteritems(self.data['objects']): for refname, (docname, ast) in iteritems(self.data['objects']):
yield (refname, refname, type, docname, refname, 1) yield (refname, refname, ast.objectType, docname, ast.newestId, 1)

View File

@ -134,6 +134,18 @@ def test_type_definitions():
check('function', 'int foo(const A&... a)') check('function', 'int foo(const A&... a)')
check('function', 'virtual void f()') check('function', 'virtual void f()')
check('class', 'public A', 'A')
check('class', 'private A')
check('enum', 'A')
check('enum', 'A : std::underlying_type<B>::type')
check('enum', 'A : unsigned int')
check('enum', 'public A', 'A')
check('enum', 'private A')
check('enumerator', 'A')
check('enumerator', 'A = std::numeric_limits<unsigned long>::max()')
def test_bases(): def test_bases():
check('class', 'A') check('class', 'A')
check('class', 'A::B::C') check('class', 'A::B::C')
@ -143,7 +155,6 @@ def test_bases():
check('class', 'A : B, C') check('class', 'A : B, C')
check('class', 'A : B, protected C, D') check('class', 'A : B, protected C, D')
def test_operators(): def test_operators():
check('function', 'void operator new [ ] ()', 'void operator new[]()') check('function', 'void operator new [ ] ()', 'void operator new[]()')
check('function', 'void operator delete ()', 'void operator delete()') check('function', 'void operator delete ()', 'void operator delete()')
@ -155,4 +166,4 @@ def test_operators():
# # used for getting all the ids out for checking # # used for getting all the ids out for checking
# for a in ids: # for a in ids:
# print(a) # print(a)
# raise DefinitionError("") # raise DefinitionError("")