diff --git a/CHANGES b/CHANGES index 41aa6fba5..c0236b418 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,8 @@ Features added * The ``language`` config value is now available in the HTML templates. * The ``env-updated`` event can now return a value, which is interpreted 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 described in documentation. @@ -27,6 +29,7 @@ Bugs fixed * #1687: linkcheck now treats 401 Unauthorized responses as "working". * #1690: toctrees with ``glob`` option now can also contain entries for single documents with explicit title. +* #1591: html search results for C++ elements now has correct interpage links. Release 1.3b2 (released Dec 5, 2014) diff --git a/doc/domains.rst b/doc/domains.rst index 590500204..7b39a37e3 100644 --- a/doc/domains.rst +++ b/doc/domains.rst @@ -520,8 +520,8 @@ The C++ Domain The C++ domain (name **cpp**) supports documenting C++ projects. -The following directives are available. All declarations can start with a visibility statement -(``public``, ``private`` or ``protected``). +The following directives are available. All declarations can start with +a visibility statement (``public``, ``private`` or ``protected``). .. 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. +.. 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::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 - 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.,:: .. cpp:namespace:: Namespace1::Namespace2::SomeClass::AnInnerClass - All following 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 + All subsequent objects will be defined as if their name were declared with the namespace + prepended. The subsequent cross-references will be searched for by both their specified name and with the namespace prepended. + Using ``NULL``, ``0``, or ``nullptr`` as the namespace will reset it to the global namespace. + .. _cpp-roles: These roles link to the given object types: .. rst:role:: cpp:class - cpp:func - cpp:member - cpp:type + cpp:func + cpp:member + cpp:type + cpp:enum + cpp:enumerator - Reference a C++ object. You can give the full specification (and need to, for - overloaded functions.) + Reference a C++ object by name. The name must be properly qualified relative to the + position of the link. .. note:: diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 9a20519fa..26f707ff8 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -121,6 +121,15 @@ # (note: only "0" is allowed as the value, according to the standard, # 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 first thing. @@ -139,7 +148,6 @@ grammar, typedef-like: no initilizer decl-specifier-seq declerator - member_object: goal: as a type_object which must have a declerator, and optionally with a initializer @@ -150,6 +158,30 @@ goal: a function declaration, TODO: what about templates? for now: skip grammar: no initializer 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 @@ -1300,12 +1332,13 @@ class ASTBaseClass(ASTBase): 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) class ASTClass(ASTBase): - def __init__(self, name, bases): + def __init__(self, name, visibility, bases): self.name = name + self.visibility = visibility self.bases = bases def get_id_v1(self): @@ -1320,6 +1353,9 @@ class ASTClass(ASTBase): def __unicode__(self): res = [] + if self.visibility != 'public': + res.append(self.visibility) + res.append(' ') res.append(text_type(self.name)) if len(self.bases) > 0: res.append(' : ') @@ -1333,6 +1369,10 @@ class ASTClass(ASTBase): def describe_signature(self, signode, mode, env): _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) if len(self.bases) > 0: signode += nodes.Text(' : ') @@ -1341,6 +1381,69 @@ class ASTClass(ASTBase): signode += nodes.Text(', ') 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): # those without signedness and size modifiers @@ -1891,6 +1994,9 @@ class DefinitionParser(object): return ASTTypeWithInit(type, init) def _parse_class(self): + classVisibility = 'public' + if self.match(_visibility_re): + classVisibility = self.matched_text name = self._parse_nested_name() bases = [] self.skip_ws() @@ -1907,7 +2013,30 @@ class DefinitionParser(object): continue else: 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): res = self._parse_type(outer='type') @@ -1929,6 +2058,16 @@ class DefinitionParser(object): res.objectType = 'class' 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): res = self._parse_nested_name() res.objectType = 'namespace' @@ -1960,6 +2099,8 @@ class CPPObject(ObjectDescription): ast.get_id_v1() ] theid = ids[0] + ast.newestId = theid + assert theid # shouldn't be None name = text_type(ast.prefixedName) if theid not in self.state.document.ids: # if the name is not unique, the first one will win @@ -1970,18 +2111,35 @@ class CPPObject(ObjectDescription): pass #print("[CPP] non-unique name:", name) 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) self.state.document.note_explicit_target(signode) - if name not in objects: - objects.setdefault(name, - (self.env.docname, ast.objectType, theid)) + if not name in objects: + objects.setdefault(name, (self.env.docname, ast)) + 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 uninstantiated = ast.prefixedName.get_name_no_last_template() if uninstantiated != name and uninstantiated not in objects: signode['names'].append(uninstantiated) - objects.setdefault(uninstantiated, ( - self.env.docname, ast.objectType, theid)) + objects.setdefault(uninstantiated, (self.env.docname, ast)) self.env.ref_context['cpp:lastname'] = ast.prefixedName indextext = self.get_index_text(name) @@ -2057,12 +2215,14 @@ class CPPClassObject(CPPObject): return _('%s (C++ class)') % name def before_content(self): - 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] + # 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() @@ -2075,6 +2235,57 @@ class CPPClassObject(CPPObject): 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): """ This directive is just to tell Sphinx that we're documenting stuff in @@ -2120,7 +2331,6 @@ class CPPXRefRole(XRefRole): title = title[dcolon + 2:] return title, target - class CPPDomain(Domain): """C++ language domain.""" name = 'cpp' @@ -2129,7 +2339,9 @@ class CPPDomain(Domain): 'class': ObjType(l_('class'), 'class'), 'function': ObjType(l_('function'), 'func'), '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 = { @@ -2137,16 +2349,22 @@ class CPPDomain(Domain): 'function': CPPFunctionObject, 'member': CPPMemberObject, 'type': CPPTypeObject, + 'enum': CPPEnumObject, + 'enum-struct': CPPEnumObject, + 'enum-class': CPPEnumObject, + 'enumerator': CPPEnumeratorObject, 'namespace': CPPNamespaceObject } roles = { 'class': CPPXRefRole(), 'func': CPPXRefRole(fix_parens=True), 'member': CPPXRefRole(), - 'type': CPPXRefRole() + 'type': CPPXRefRole(), + 'enum': CPPXRefRole(), + 'enumerator': CPPXRefRole() } initial_data = { - 'objects': {}, # prefixedName -> (docname, objectType, id) + 'objects': {}, # prefixedName -> (docname, ast) } def clear_doc(self, docname): @@ -2169,9 +2387,9 @@ class CPPDomain(Domain): name = nameAst.get_name_no_last_template() if name not in self.data['objects']: return None, None - docname, objectType, id = self.data['objects'][name] - return make_refnode(builder, fromdocname, docname, id, contnode, - name), objectType + docname, ast = self.data['objects'][name] + return make_refnode(builder, fromdocname, docname, ast.newestId, + contnode, name), ast.objectType parser = DefinitionParser(target) try: @@ -2210,5 +2428,5 @@ class CPPDomain(Domain): return [] def get_objects(self): - for refname, (docname, type, theid) in iteritems(self.data['objects']): - yield (refname, refname, type, docname, refname, 1) + for refname, (docname, ast) in iteritems(self.data['objects']): + yield (refname, refname, ast.objectType, docname, ast.newestId, 1) diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index e690394da..98cbd294f 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -134,6 +134,18 @@ def test_type_definitions(): check('function', 'int foo(const A&... a)') check('function', 'virtual void f()') + check('class', 'public A', 'A') + check('class', 'private A') + + check('enum', 'A') + check('enum', 'A : std::underlying_type::type') + check('enum', 'A : unsigned int') + check('enum', 'public A', 'A') + check('enum', 'private A') + + check('enumerator', 'A') + check('enumerator', 'A = std::numeric_limits::max()') + def test_bases(): check('class', 'A') check('class', 'A::B::C') @@ -143,7 +155,6 @@ def test_bases(): check('class', 'A : B, C') check('class', 'A : B, protected C, D') - def test_operators(): check('function', 'void operator new [ ] ()', 'void operator new[]()') check('function', 'void operator delete ()', 'void operator delete()') @@ -155,4 +166,4 @@ def test_operators(): # # used for getting all the ids out for checking # for a in ids: # print(a) -# raise DefinitionError("") \ No newline at end of file +# raise DefinitionError("")