From 50ae1b04e8bc6249a3b1537b88aa7769d8533dba Mon Sep 17 00:00:00 2001 From: Jan Babst Date: Sat, 18 Apr 2020 00:26:10 +0200 Subject: [PATCH 1/4] C++ domain: Add support for semicolon in declarations --- sphinx/domains/cpp.py | 27 +++++++++++++++++++++------ tests/test_domain_cpp.py | 13 ++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 7915f4128..7af2b0c37 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -109,7 +109,7 @@ 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). + # For now: drop the attributes (TODO). # Use at most 1 init-declarator. -> decl-specifier-seq init-declarator -> decl-specifier-seq declarator initializer @@ -2690,11 +2690,13 @@ class ASTInitializer(ASTBase): class ASTType(ASTBase): - def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator, + semicolon: bool = False) -> None: assert declSpecs assert decl self.declSpecs = declSpecs self.decl = decl + self.semicolon = semicolon @property def name(self) -> ASTNestedName: @@ -2768,6 +2770,8 @@ class ASTType(ASTBase): if self.decl.require_space_after_declSpecs() and len(declSpecs) > 0: res.append(' ') res.append(transform(self.decl)) + if self.semicolon: + res.append(';') return ''.join(res) def get_type_declaration_prefix(self) -> str: @@ -2788,6 +2792,8 @@ class ASTType(ASTBase): if mode == 'markType': mode = 'noneIsName' self.decl.describe_signature(signode, mode, env, symbol) + if self.semicolon: + signode += nodes.Text(';') class ASTTemplateParamConstrainedTypeWithInit(ASTBase): @@ -3465,12 +3471,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 +3491,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 +3533,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 +3588,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): @@ -5864,6 +5876,7 @@ class DefinitionParser(BaseParser): raise Exception('Internal error, unknown outer "%s".' % outer) if outer != 'operatorCast': assert named + semicolon = False if outer in ('type', 'function'): # We allow type objects to just be a name. # Some functions don't have normal return types: constructors, @@ -5875,6 +5888,7 @@ class DefinitionParser(BaseParser): declSpecs = self._parse_decl_specs(outer=outer, typed=False) decl = self._parse_declarator(named=True, paramMode=outer, typed=False) + semicolon = self.skip_string_and_ws(';') self.assert_end() except DefinitionError as exUntyped: if outer == 'type': @@ -5930,7 +5944,7 @@ class DefinitionParser(BaseParser): named = 'single' declSpecs = self._parse_decl_specs(outer=outer) decl = self._parse_declarator(named=named, paramMode=paramMode) - return ASTType(declSpecs, decl) + return ASTType(declSpecs, decl, semicolon) def _parse_type_with_init( self, named: Union[bool, str], @@ -6286,8 +6300,9 @@ class DefinitionParser(BaseParser): templatePrefix, fullSpecShorthand=False, isMember=objectType == 'member') + semicolon = self.skip_string_and_ws(';') 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/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 0b757139a..ae3e1fa1e 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(): From ef0c2bf83c76e1657f153d62681c32af71b0cae6 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Fri, 24 Apr 2020 10:57:23 +0200 Subject: [PATCH 2/4] C++, semicolon, move it entirely to ASTDeclaration --- sphinx/domains/cpp.py | 18 ++++++------------ sphinx/util/cfamily.py | 10 +++++++--- tests/roots/test-domain-cpp/semicolon.rst | 14 ++++++++++++++ tests/test_domain_cpp.py | 9 ++++++++- 4 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 tests/roots/test-domain-cpp/semicolon.rst diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 7af2b0c37..d6c8cefd1 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -109,6 +109,7 @@ T = TypeVar('T') simple-declaration -> attribute-specifier-seq[opt] decl-specifier-seq[opt] init-declarator-list[opt] ; + # Make the semicolon optional. # For now: drop the attributes (TODO). # Use at most 1 init-declarator. -> decl-specifier-seq init-declarator @@ -2690,13 +2691,11 @@ class ASTInitializer(ASTBase): class ASTType(ASTBase): - def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator, - semicolon: bool = False) -> None: + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: assert declSpecs assert decl self.declSpecs = declSpecs self.decl = decl - self.semicolon = semicolon @property def name(self) -> ASTNestedName: @@ -2770,8 +2769,6 @@ class ASTType(ASTBase): if self.decl.require_space_after_declSpecs() and len(declSpecs) > 0: res.append(' ') res.append(transform(self.decl)) - if self.semicolon: - res.append(';') return ''.join(res) def get_type_declaration_prefix(self) -> str: @@ -2792,8 +2789,6 @@ class ASTType(ASTBase): if mode == 'markType': mode = 'noneIsName' self.decl.describe_signature(signode, mode, env, symbol) - if self.semicolon: - signode += nodes.Text(';') class ASTTemplateParamConstrainedTypeWithInit(ASTBase): @@ -5876,7 +5871,6 @@ class DefinitionParser(BaseParser): raise Exception('Internal error, unknown outer "%s".' % outer) if outer != 'operatorCast': assert named - semicolon = False if outer in ('type', 'function'): # We allow type objects to just be a name. # Some functions don't have normal return types: constructors, @@ -5888,8 +5882,7 @@ class DefinitionParser(BaseParser): declSpecs = self._parse_decl_specs(outer=outer, typed=False) decl = self._parse_declarator(named=True, paramMode=outer, typed=False) - semicolon = self.skip_string_and_ws(';') - self.assert_end() + self.assert_end(allowSemicolon=True) except DefinitionError as exUntyped: if outer == 'type': desc = "If just a name" @@ -5944,7 +5937,7 @@ class DefinitionParser(BaseParser): named = 'single' declSpecs = self._parse_decl_specs(outer=outer) decl = self._parse_declarator(named=named, paramMode=paramMode) - return ASTType(declSpecs, decl, semicolon) + return ASTType(declSpecs, decl) def _parse_type_with_init( self, named: Union[bool, str], @@ -6300,7 +6293,8 @@ class DefinitionParser(BaseParser): templatePrefix, fullSpecShorthand=False, isMember=objectType == 'member') - semicolon = self.skip_string_and_ws(';') + self.skip_ws() + semicolon = self.skip_string(';') return ASTDeclaration(objectType, directiveType, visibility, templatePrefix, declaration, semicolon) 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-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_cpp.py b/tests/test_domain_cpp.py index ae3e1fa1e..5a41b6dd2 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -87,7 +87,7 @@ def check(name, input, idDict, output=None): # First, check without semicolon _check(name, input, idDict, output) # Second, check with semicolon - _check(name, input + ';', idDict, output + ';') + _check(name, input + ' ;', idDict, output + ';') def test_fundamental_types(): @@ -910,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): From abe65423ca6ca5cfc4fa3420015e8ce26f64e9cf Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Fri, 24 Apr 2020 11:38:22 +0200 Subject: [PATCH 3/4] C, allow semicolon in the end of declarations (except macros) --- sphinx/domains/c.py | 20 ++++++++++++++++---- tests/roots/test-domain-c/semicolon.rst | 10 ++++++++++ tests/test_domain_c.py | 25 +++++++++++++++++++++---- 3 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 tests/roots/test-domain-c/semicolon.rst 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/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/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)") From f009523bd3ee2f1d762d4a88a3c2547578c4e672 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Fri, 24 Apr 2020 11:41:50 +0200 Subject: [PATCH 4/4] Update CHANGES --- CHANGES | 1 + 1 file changed, 1 insertion(+) 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 ----------