diff --git a/CHANGES b/CHANGES index bebfc5500..aacf2f5b2 100644 --- a/CHANGES +++ b/CHANGES @@ -54,6 +54,7 @@ Features added * #7533: html theme: Avoid whitespace at the beginning of genindex.html * #7541: html theme: Add a "clearer" at the end of the "body" * #7542: html theme: Make admonition/topic/sidebar scrollable +* C and C++: allow semicolon in the end of declarations. Bugs fixed ---------- diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 35641ec8c..1e5eb57a0 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -1272,10 +1272,12 @@ class ASTEnumerator(ASTBase): class ASTDeclaration(ASTBaseBase): - def __init__(self, objectType: str, directiveType: str, declaration: Any) -> None: + def __init__(self, objectType: str, directiveType: str, declaration: Any, + semicolon: bool = False) -> None: self.objectType = objectType self.directiveType = directiveType self.declaration = declaration + self.semicolon = semicolon self.symbol = None # type: Symbol # set by CObject._add_enumerator_to_parent @@ -1304,7 +1306,10 @@ class ASTDeclaration(ASTBaseBase): return self.get_id(_max_id, True) def _stringify(self, transform: StringifyTransform) -> str: - return transform(self.declaration) + res = transform(self.declaration) + if self.semicolon: + res += ';' + return res def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", options: Dict) -> None: @@ -1340,6 +1345,8 @@ class ASTDeclaration(ASTBaseBase): else: assert False self.declaration.describe_signature(mainDeclNode, mode, env, self.symbol) + if self.semicolon: + mainDeclNode += nodes.Text(';') class SymbolLookupResult: @@ -2742,7 +2749,7 @@ class DefinitionParser(BaseParser): declSpecs = self._parse_decl_specs(outer=outer, typed=False) decl = self._parse_declarator(named=True, paramMode=outer, typed=False) - self.assert_end() + self.assert_end(allowSemicolon=True) except DefinitionError as exUntyped: desc = "If just a name" prevErrors.append((exUntyped, desc)) @@ -2875,7 +2882,12 @@ class DefinitionParser(BaseParser): declaration = self._parse_type(named=True, outer='type') else: assert False - return ASTDeclaration(objectType, directiveType, declaration) + if objectType != 'macro': + self.skip_ws() + semicolon = self.skip_string(';') + else: + semicolon = False + return ASTDeclaration(objectType, directiveType, declaration, semicolon) def parse_namespace_object(self) -> ASTNestedName: return self._parse_nested_name() diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 7915f4128..d6c8cefd1 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -109,7 +109,8 @@ T = TypeVar('T') simple-declaration -> attribute-specifier-seq[opt] decl-specifier-seq[opt] init-declarator-list[opt] ; - # Drop the semi-colon. For now: drop the attributes (TODO). + # Make the semicolon optional. + # For now: drop the attributes (TODO). # Use at most 1 init-declarator. -> decl-specifier-seq init-declarator -> decl-specifier-seq declarator initializer @@ -3465,12 +3466,14 @@ class ASTTemplateDeclarationPrefix(ASTBase): class ASTDeclaration(ASTBase): def __init__(self, objectType: str, directiveType: str, visibility: str, - templatePrefix: ASTTemplateDeclarationPrefix, declaration: Any) -> None: + templatePrefix: ASTTemplateDeclarationPrefix, declaration: Any, + semicolon: bool = False) -> None: self.objectType = objectType self.directiveType = directiveType self.visibility = visibility self.templatePrefix = templatePrefix self.declaration = declaration + self.semicolon = semicolon self.symbol = None # type: Symbol # set by CPPObject._add_enumerator_to_parent @@ -3483,7 +3486,7 @@ class ASTDeclaration(ASTBase): templatePrefixClone = None return ASTDeclaration(self.objectType, self.directiveType, self.visibility, templatePrefixClone, - self.declaration.clone()) + self.declaration.clone(), self.semicolon) @property def name(self) -> ASTNestedName: @@ -3525,6 +3528,8 @@ class ASTDeclaration(ASTBase): if self.templatePrefix: res.append(transform(self.templatePrefix)) res.append(transform(self.declaration)) + if self.semicolon: + res.append(';') return ''.join(res) def describe_signature(self, signode: desc_signature, mode: str, @@ -3578,6 +3583,8 @@ class ASTDeclaration(ASTBase): else: assert False self.declaration.describe_signature(mainDeclNode, mode, env, self.symbol) + if self.semicolon: + mainDeclNode += nodes.Text(';') class ASTNamespace(ASTBase): @@ -5875,7 +5882,7 @@ class DefinitionParser(BaseParser): declSpecs = self._parse_decl_specs(outer=outer, typed=False) decl = self._parse_declarator(named=True, paramMode=outer, typed=False) - self.assert_end() + self.assert_end(allowSemicolon=True) except DefinitionError as exUntyped: if outer == 'type': desc = "If just a name" @@ -6286,8 +6293,10 @@ class DefinitionParser(BaseParser): templatePrefix, fullSpecShorthand=False, isMember=objectType == 'member') + self.skip_ws() + semicolon = self.skip_string(';') return ASTDeclaration(objectType, directiveType, visibility, - templatePrefix, declaration) + templatePrefix, declaration, semicolon) def parse_namespace_object(self) -> ASTNamespace: templatePrefix = self._parse_template_declaration_prefix(objectType="namespace") diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index cdac9231f..790a492a5 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -338,10 +338,14 @@ class BaseParser: self.pos = self.end return rv - def assert_end(self) -> None: + def assert_end(self, *, allowSemicolon: bool = False) -> None: self.skip_ws() - if not self.eof: - self.fail('Expected end of definition.') + if allowSemicolon: + if not self.eof and self.definition[self.pos:] != ';': + self.fail('Expected end of definition or ;.') + else: + if not self.eof: + self.fail('Expected end of definition.') ################################################################################ diff --git a/tests/roots/test-domain-c/semicolon.rst b/tests/roots/test-domain-c/semicolon.rst new file mode 100644 index 000000000..14ba17756 --- /dev/null +++ b/tests/roots/test-domain-c/semicolon.rst @@ -0,0 +1,10 @@ +.. c:member:: int member; +.. c:var:: int var; +.. c:function:: void f(); +.. .. c:macro:: NO_SEMICOLON; +.. c:struct:: Struct; +.. c:union:: Union; +.. c:enum:: Enum; +.. c:enumerator:: Enumerator; +.. c:type:: Type; +.. c:type:: int TypeDef; diff --git a/tests/roots/test-domain-cpp/semicolon.rst b/tests/roots/test-domain-cpp/semicolon.rst new file mode 100644 index 000000000..e6b370ea5 --- /dev/null +++ b/tests/roots/test-domain-cpp/semicolon.rst @@ -0,0 +1,14 @@ +.. cpp:class:: Class; +.. cpp:struct:: Struct; +.. cpp:union:: Union; +.. cpp:function:: void f(); +.. cpp:member:: int member; +.. cpp:var:: int var; +.. cpp:type:: Type; +.. cpp:type:: int TypeDef; +.. cpp:type:: Alias = int; +.. cpp:concept:: template Concept; +.. cpp:enum:: Enum; +.. cpp:enum-struct:: EnumStruct; +.. cpp:enum-class:: EnumClass; +.. cpp:enumerator:: Enumerator; diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 9003532e1..f85a0e62e 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -27,10 +27,8 @@ def parse(name, string): return ast -def check(name, input, idDict, output=None): +def _check(name, input, idDict, output): # first a simple check of the AST - if output is None: - output = input ast = parse(name, input) res = str(ast) if res != output: @@ -77,6 +75,16 @@ def check(name, input, idDict, output=None): raise DefinitionError("") +def check(name, input, idDict, output=None): + if output is None: + output = input + # First, check without semicolon + _check(name, input, idDict, output) + if name != 'macro': + # Second, check with semicolon + _check(name, input + ' ;', idDict, output + ';') + + def test_expressions(): def exprCheck(expr, output=None): class Config: @@ -469,8 +477,9 @@ def test_build_domain_c(app, status, warning): ws = filter_warnings(warning, "index") assert len(ws) == 0 + @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) -def test_build_domain_c(app, status, warning): +def test_build_domain_c_namespace(app, status, warning): app.builder.build_all() ws = filter_warnings(warning, "namespace") assert len(ws) == 0 @@ -478,6 +487,7 @@ def test_build_domain_c(app, status, warning): for id_ in ('NS.NSVar', 'NULLVar', 'ZeroVar', 'NS2.NS3.NS2NS3Var', 'PopVar'): assert 'id="c.{}"'.format(id_) in t + @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_build_domain_c_anon_dup_decl(app, status, warning): app.builder.build_all() @@ -487,6 +497,13 @@ def test_build_domain_c_anon_dup_decl(app, status, warning): assert "WARNING: c:identifier reference target not found: @b" in ws[1] +@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) +def test_build_domain_c_semicolon(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "semicolon") + assert len(ws) == 0 + + def test_cfunction(app): text = (".. c:function:: PyObject* " "PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)") diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 0b757139a..5a41b6dd2 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -33,10 +33,8 @@ def parse(name, string): return ast -def check(name, input, idDict, output=None): +def _check(name, input, idDict, output): # first a simple check of the AST - if output is None: - output = input ast = parse(name, input) res = str(ast) if res != output: @@ -83,6 +81,15 @@ def check(name, input, idDict, output=None): raise DefinitionError("") +def check(name, input, idDict, output=None): + if output is None: + output = input + # First, check without semicolon + _check(name, input, idDict, output) + # Second, check with semicolon + _check(name, input + ' ;', idDict, output + ';') + + def test_fundamental_types(): # see https://en.cppreference.com/w/cpp/language/types for t, id_v2 in cppDomain._id_fundamental_v2.items(): @@ -903,6 +910,13 @@ def test_build_domain_cpp_backslash_ok(app, status, warning): assert len(ws) == 0 +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_build_domain_cpp_semicolon(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "semicolon") + assert len(ws) == 0 + + @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True, 'strip_signature_backslash': True}) def test_build_domain_cpp_backslash_ok(app, status, warning):