diff --git a/CHANGES b/CHANGES index f9f267c3a90..0e03a3f0a13 100644 --- a/CHANGES +++ b/CHANGES @@ -77,6 +77,8 @@ Bugs fixed with size explicitly set in pixels) (fixed for ``'pdflatex'/'lualatex'`` only) * #8911: C++: remove the longest matching prefix in :confval:`cpp_index_common_prefix` instead of the first that matches. +* C, properly reject function declarations when a keyword is used + as parameter name. Testing -------- diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 061010d6651..458467e6abc 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -9,8 +9,8 @@ """ import re -from typing import (Any, Callable, Dict, Generator, Iterator, List, Tuple, Type, TypeVar, - Union, cast) +from typing import (Any, Callable, Dict, Generator, Iterator, List, Optional, Tuple, Type, + TypeVar, Union, cast) from docutils import nodes from docutils.nodes import Element, Node, TextElement, system_message @@ -78,8 +78,8 @@ _expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", ">>=", "<<=", "&=", "and_eq", "^=", "xor_eq", "|=", "or_eq"] -_max_id = 1 -_id_prefix = [None, 'c.', 'Cv2.'] +_max_id = 2 +_id_prefix = [None, 'c.', 'C2-'] # Ids are used in lookup keys which are used across pickled files, # so when _max_id changes, make sure to update the ENV_VERSION. @@ -108,31 +108,59 @@ def describe_signature(self, signode: TextElement, mode: str, ################################################################################ class ASTIdentifier(ASTBaseBase): - def __init__(self, identifier: str) -> None: + def __init__(self, identifier: str, tag: Optional[str]) -> None: + # tag: + # - len > 0: it's a struct/union/enum + # - len == 0: it's not a struct/union/enum + # - None: for matching, can be either assert identifier is not None assert len(identifier) != 0 self.identifier = identifier - - def __eq__(self, other: Any) -> bool: - return type(other) is ASTIdentifier and self.identifier == other.identifier + self.tag = tag def is_anon(self) -> bool: return self.identifier[0] == '@' + def get_id(self, version: int) -> str: + if version <= 1: + return self.identifier + if self.tag: + assert len(self.tag) != 0 + return '-' + self.identifier + else: + return self.identifier + + def matches(self, other: "ASTIdentifier") -> bool: + if self.identifier != other.identifier: + return False + if self.tag is None: + return True + assert other.tag is not None + isTag = self.tag == '' + isOtherTag = other.tag == '' + return isTag == isOtherTag + # and this is where we finally make a difference between __str__ and the display string def __str__(self) -> str: - return self.identifier + return self.tag + " " + self.identifier if self.tag else self.identifier def get_display_string(self) -> str: - return "[anonymous]" if self.is_anon() else self.identifier + id = "[anonymous]" if self.is_anon() else self.identifier + return self.tag + " " + id if self.tag else id def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", prefix: str, symbol: "Symbol") -> None: # note: slightly different signature of describe_signature due to the prefix verify_description_mode(mode) + if self.tag: + signode += nodes.Text(self.tag) + signode += nodes.Text(' ') if mode == 'markType': - targetText = prefix + self.identifier + if self.tag: + targetText = prefix + self.tag + ' ' + self.identifier + else: + targetText = prefix + self.identifier pnode = addnodes.pending_xref('', refdomain='c', reftype='identifier', reftarget=targetText, modname=None, @@ -163,12 +191,17 @@ def __init__(self, names: List[ASTIdentifier], rooted: bool) -> None: self.names = names self.rooted = rooted + def setTagsToPattern(self) -> None: + for n in self.names: + if n.tag == '': + n.tag = None + @property def name(self) -> "ASTNestedName": return self def get_id(self, version: int) -> str: - return '.'.join(str(n) for n in self.names) + return '.'.join(n.get_id(version) for n in self.names) def _stringify(self, transform: StringifyTransform) -> str: res = '.'.join(transform(n) for n in self.names) @@ -387,19 +420,6 @@ def describe_signature(self, signode: TextElement, mode: str, signode.append(nodes.Text('--')) -class ASTPostfixMember(ASTPostfixOp): - def __init__(self, name): - self.name = name - - def _stringify(self, transform: StringifyTransform) -> str: - return '.' + transform(self.name) - - def describe_signature(self, signode: TextElement, mode: str, - env: "BuildEnvironment", symbol: "Symbol") -> None: - signode.append(nodes.Text('.')) - self.name.describe_signature(signode, 'noneIsName', env, symbol) - - class ASTPostfixMemberOfPointer(ASTPostfixOp): def __init__(self, name): self.name = name @@ -607,8 +627,7 @@ def describe_signature(self, signode: TextElement, mode: str, class ASTTrailingTypeSpecName(ASTTrailingTypeSpec): - def __init__(self, prefix: str, nestedName: ASTNestedName) -> None: - self.prefix = prefix + def __init__(self, nestedName: ASTNestedName) -> None: self.nestedName = nestedName @property @@ -616,18 +635,10 @@ def name(self) -> ASTNestedName: return self.nestedName def _stringify(self, transform: StringifyTransform) -> str: - res = [] - if self.prefix: - res.append(self.prefix) - res.append(' ') - res.append(transform(self.nestedName)) - return ''.join(res) + return transform(self.nestedName) def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnvironment", symbol: "Symbol") -> None: - if self.prefix: - signode += addnodes.desc_annotation(self.prefix, self.prefix) - signode += nodes.Text(' ') self.nestedName.describe_signature(signode, mode, env, symbol=symbol) @@ -1678,7 +1689,7 @@ def candidates() -> Generator["Symbol", None, None]: if Symbol.debug_lookup: Symbol.debug_print("candidate:") print(s.to_string(Symbol.debug_indent + 1), end="") - if s.ident == ident: + if s.ident and ident.matches(s.ident): if Symbol.debug_lookup: Symbol.debug_indent += 1 Symbol.debug_print("matches") @@ -1875,23 +1886,9 @@ def handleDuplicateDeclaration(symbol: "Symbol", candSymbol: "Symbol") -> None: candSymbol.isRedeclaration = True raise _DuplicateSymbolError(symbol, declaration) - if declaration.objectType != "function": - assert len(withDecl) <= 1 - handleDuplicateDeclaration(withDecl[0], candSymbol) - # (not reachable) - - # a function, so compare IDs - candId = declaration.get_newest_id() - if Symbol.debug_lookup: - Symbol.debug_print("candId:", candId) - for symbol in withDecl: - oldId = symbol.declaration.get_newest_id() - if Symbol.debug_lookup: - Symbol.debug_print("oldId: ", oldId) - if candId == oldId: - handleDuplicateDeclaration(symbol, candSymbol) - # (not reachable) - # no candidate symbol found with matching ID + assert len(withDecl) <= 1 + handleDuplicateDeclaration(withDecl[0], candSymbol) + # (not reachable) # if there is an empty symbol, fill that one if len(noDecl) == 0: if Symbol.debug_lookup: @@ -1973,8 +1970,12 @@ def add_declaration(self, declaration: ASTDeclaration, Symbol.debug_print("add_declaration:") assert declaration is not None assert docname is not None - assert line is not None - nestedName = declaration.name + assert declaration.name.names[-1].tag == '' + if declaration.objectType in ('struct', 'union', 'enum'): + nestedName = declaration.name.clone() + nestedName.names[-1].tag = declaration.objectType + else: + nestedName = declaration.name res = self._add_symbols(nestedName, declaration, docname, line) if Symbol.debug_lookup: Symbol.debug_indent -= 1 @@ -2000,11 +2001,11 @@ def find_identifier(self, ident: ASTIdentifier, Symbol.debug_print("trying:") print(current.to_string(Symbol.debug_indent + 1), end="") Symbol.debug_indent -= 2 - if matchSelf and current.ident == ident: + if matchSelf and ident.matches(current.ident): return current children = current.children_recurse_anon if recurseInAnon else current._children for s in children: - if s.ident == ident: + if ident.matches(s.ident): return s if not searchInSiblings: break @@ -2102,8 +2103,6 @@ class DefinitionParser(BaseParser): '__int64', ) - _prefix_keys = ('struct', 'enum', 'union') - @property def language(self) -> str: return 'C' @@ -2195,10 +2194,9 @@ def _parse_primary_expression(self) -> ASTExpression: res = self._parse_paren_expression() if res is not None: return res - nn = self._parse_nested_name() - if nn is not None: - return ASTIdExpression(nn) - return None + nn = self._parse_nested_name(allowTags=None) + assert nn is not None + return ASTIdExpression(nn) def _parse_initializer_list(self, name: str, open: str, close: str ) -> Tuple[List[ASTExpression], bool]: @@ -2256,7 +2254,7 @@ def _parse_postfix_expression(self) -> ASTPostfixExpr: # | postfix "[" expression "]" # | postfix "[" braced-init-list [opt] "]" # | postfix "(" expression-list [opt] ")" - # | postfix "." id-expression + # | postfix "." id-expression // taken care of in primary by nested name # | postfix "->" id-expression # | postfix "++" # | postfix "--" @@ -2274,23 +2272,12 @@ def _parse_postfix_expression(self) -> ASTPostfixExpr: self.fail("Expected ']' in end of postfix expression.") postFixes.append(ASTPostfixArray(expr)) continue - if self.skip_string('.'): - if self.skip_string('*'): - # don't steal the dot - self.pos -= 2 - elif self.skip_string('..'): - # don't steal the dot - self.pos -= 3 - else: - name = self._parse_nested_name() - postFixes.append(ASTPostfixMember(name)) - continue if self.skip_string('->'): if self.skip_string('*'): # don't steal the arrow self.pos -= 3 else: - name = self._parse_nested_name() + name = self._parse_nested_name(allowTags=None) postFixes.append(ASTPostfixMemberOfPointer(name)) continue if self.skip_string('++'): @@ -2508,15 +2495,26 @@ def _parse_expression_fallback( value = self.definition[startPos:self.pos].strip() return ASTFallbackExpr(value.strip()) - def _parse_nested_name(self) -> ASTNestedName: - names = [] # type: List[Any] + def _parse_nested_name(self, *, allowTags: Optional[str] = 'not last') -> ASTNestedName: + # A dot-separated list of identifiers, each with an optional struct/union/enum tag. + # A leading dot makes the name rooted at global scope. + + assert allowTags in (None, 'all', 'not last') + + names = [] # type: List[ASTIdentifier] - self.skip_ws() rooted = False if self.skip_string('.'): rooted = True while 1: self.skip_ws() + tag = '' + if allowTags is not None: + for k in ('struct', 'union', 'enum'): + if self.skip_word_and_ws(k): + tag = k + break + afterTagPos = self.pos if not self.match(identifier_re): self.fail("Expected identifier in nested name.") identifier = self.matched_text @@ -2524,13 +2522,19 @@ def _parse_nested_name(self) -> ASTNestedName: if identifier in _keywords: self.fail("Expected identifier in nested name, " "got keyword: %s" % identifier) - ident = ASTIdentifier(identifier) + ident = ASTIdentifier(identifier, tag) names.append(ident) self.skip_ws() if not self.skip_string('.'): break - return ASTNestedName(names, rooted) + + # post hax to explicitly forbid the last one to have a tag + if allowTags == 'not last' and names[-1].tag != '': + self.pos = afterTagPos + self.fail("Expected identifier in nested name, " + "got keyword: %s" % names[-1].tag) + return ASTNestedName(names, rooted=rooted) def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: # fundamental types @@ -2563,16 +2567,8 @@ def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: if len(elements) > 0: return ASTTrailingTypeSpecFundamental(' '.join(elements)) - # prefixed - prefix = None - self.skip_ws() - for k in self._prefix_keys: - if self.skip_word_and_ws(k): - prefix = k - break - - nestedName = self._parse_nested_name() - return ASTTrailingTypeSpecName(prefix, nestedName) + nestedName = self._parse_nested_name(allowTags='all') + return ASTTrailingTypeSpecName(nestedName) def _parse_parameters(self, paramMode: str) -> ASTParameters: self.skip_ws() @@ -2693,17 +2689,14 @@ def _parse_decl_specs(self, outer: str, typed: bool = True) -> ASTDeclSpecs: def _parse_declarator_name_suffix( self, named: Union[bool, str], paramMode: str, typed: bool ) -> ASTDeclarator: + assert named in (True, False, 'single') # now we should parse the name, and then suffixes - if named == 'maybe': - pos = self.pos - try: - declId = self._parse_nested_name() - except DefinitionError: - self.pos = pos - declId = None - elif named == 'single': + if named == 'single': if self.match(identifier_re): - identifier = ASTIdentifier(self.matched_text) + if self.matched_text in _keywords: + self.fail("Expected identifier, " + "got keyword: %s" % self.matched_text) + identifier = ASTIdentifier(self.matched_text, '') declId = ASTNestedName([identifier], rooted=False) else: declId = None @@ -2880,8 +2873,8 @@ def parser(): def _parse_type(self, named: Union[bool, str], outer: str = None) -> ASTType: """ - named=False|'maybe'|True: 'maybe' is e.g., for function objects which - doesn't need to name the arguments + named=False|'single'|True: 'single' is e.g., for function objects which + doesn't need to name the arguments, but otherwise is a single name """ if outer: # always named if outer not in ('type', 'member', 'function'): @@ -2965,7 +2958,7 @@ def _parse_macro(self) -> ASTMacro: break if not self.match(identifier_re): self.fail("Expected identifier in macro parameters.") - nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False) + nn = ASTNestedName([ASTIdentifier(self.matched_text, '')], rooted=False) # Allow named variadic args: # https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html self.skip_ws() @@ -3062,10 +3055,10 @@ def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclarati return ASTDeclaration(objectType, directiveType, declaration, semicolon) def parse_namespace_object(self) -> ASTNestedName: - return self._parse_nested_name() + return self._parse_nested_name(allowTags='all') def parse_xref_object(self) -> ASTNestedName: - name = self._parse_nested_name() + name = self._parse_nested_name(allowTags='all') # if there are '()' left, just skip them self.skip_ws() self.skip_string('()') @@ -3095,7 +3088,7 @@ def parse_expression(self) -> Union[ASTExpression, ASTType]: def _make_phony_error_name() -> ASTNestedName: - return ASTNestedName([ASTIdentifier("PhonyNameDueToError")], rooted=False) + return ASTNestedName([ASTIdentifier("PhonyNameDueToError", None)], rooted=False) class CObject(ObjectDescription[ASTDeclaration]): @@ -3809,6 +3802,11 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: logger.warning('Unparseable C cross-reference: %r\n%s', target, e, location=node) return None, None + if typ in ('struct', 'union', 'enum'): + last = name.names[-1] + if last.tag == '': + last.tag = typ + parentKey = node.get("c:parent_key", None) # type: LookupKey rootSymbol = self.data['root_symbol'] if parentKey: @@ -3820,10 +3818,61 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: assert parentSymbol # should be there else: parentSymbol = rootSymbol + s = parentSymbol.find_declaration(name, typ, matchSelf=True, recurseInAnon=True) if s is None or s.declaration is None: - return None, None + # try with relaxed tagging + name.setTagsToPattern() + s = parentSymbol.find_declaration(name, typ, + matchSelf=True, recurseInAnon=True) + if s is None or s.declaration is None: + return None, None + # only warn for identifiers, as they should contain the correct tagging + if typ == 'identifier': + logger.warning( + "c:%s reference has incorrect tagging:" + " Full reference name is '%s'." + " Full found name is '%s'.", + typ, name, s.get_full_nested_name(), location=node) + # TODO: conditionally warn about xrefs with incorrect tagging? + + # check if tags are used correctly + sName = s.get_full_nested_name() + assert len(name.names) <= len(sName.names) + nextName = len(sName.names) - 1 + # print("Matching '{}' to '{}'".format(name, sName)) + for xRefName in reversed(name.names): + # find the next symbol name that matches the xref name + # potentially skipping anon names + # print("\tMatching:", xRefName) + stop = False + while True: + if nextName == -1: + stop = True + break + ns = sName.names[nextName] + nextName -= 1 + if xRefName.identifier == ns.identifier: + # print("\t\tSame ident:", ns) + break + else: + # print("\t\tSkipping:", ns) + assert ns.is_anon() + if stop: + break + # print("\t\tRes(nextName={}): '{}' vs. '{}'".format(nextName, xRefName, ns)) + if xRefName.tag is None: + continue + assert ns.tag is not None + assert (xRefName.tag == '') == (ns.tag == '') + if xRefName.tag != ns.tag: + logger.warning( + "c:%s reference uses wrong tag:" + " reference name is '%s' but found name is '%s'." + " Full reference name is '%s'." + " Full found name is '%s'.", + typ, xRefName, ns, name, sName, location=node) # TODO: check role type vs. object type @@ -3876,9 +3925,18 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value("c_allow_pre_v3", False, 'env') app.add_config_value("c_warn_on_allowed_pre_v3", True, 'env') + # debug stuff + app.add_config_value("c_debug_lookup", False, '') + app.add_config_value("c_debug_show_tree", False, '') + + def setDebugFlags(app): + Symbol.debug_lookup = app.config.c_debug_lookup + Symbol.debug_show_tree = app.config.c_debug_show_tree + app.connect("builder-inited", setDebugFlags) + return { 'version': 'builtin', - 'env_version': 2, + 'env_version': 3, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/tests/roots/test-domain-c-intersphinx/index.rst b/tests/roots/test-domain-c-intersphinx/index.rst index 5d6d3e09891..c97cc695c0e 100644 --- a/tests/roots/test-domain-c-intersphinx/index.rst +++ b/tests/roots/test-domain-c-intersphinx/index.rst @@ -60,3 +60,12 @@ - :c:member:`_functionParam.param` - :c:var:`_functionParam.param` - :c:data:`_functionParam.param` + +- :any:`_struct.@anon.i` +- :c:member:`_struct.@anon.i` +- :c:var:`_struct.@anon.i` +- :c:data:`_struct.@anon.i` +- :any:`_struct.i` +- :c:member:`_struct.i` +- :c:var:`_struct.i` +- :c:data:`_struct.i` diff --git a/tests/roots/test-domain-c/anon-dup-decl.rst b/tests/roots/test-domain-c/anon-dup-decl.rst index 5f6c3bdfe36..743ae2f6a84 100644 --- a/tests/roots/test-domain-c/anon-dup-decl.rst +++ b/tests/roots/test-domain-c/anon-dup-decl.rst @@ -1,3 +1,5 @@ +.. c:namespace:: anon_dup_decl_ns + .. c:struct:: anon_dup_decl .. c:struct:: @a.A diff --git a/tests/roots/test-domain-c/function_param_target.rst b/tests/roots/test-domain-c/function_param_target.rst index 05de01445d4..d316d7bcd16 100644 --- a/tests/roots/test-domain-c/function_param_target.rst +++ b/tests/roots/test-domain-c/function_param_target.rst @@ -1,3 +1,5 @@ +.. c:namespace:: function_param_target + .. c:function:: void f(int i) - :c:var:`i` diff --git a/tests/roots/test-domain-c/ids-vs-tags0.rst b/tests/roots/test-domain-c/ids-vs-tags0.rst new file mode 100644 index 00000000000..f00d3b79570 --- /dev/null +++ b/tests/roots/test-domain-c/ids-vs-tags0.rst @@ -0,0 +1,90 @@ +.. c:member:: int _member +.. c:var:: int _var +.. c:function:: void _function() +.. c:macro:: _macro +.. c:struct:: _struct + + .. c:union:: @anon + + .. c:var:: int i + +.. c:union:: _union +.. c:enum:: _enum + + .. c:enumerator:: _enumerator + +.. c:type:: _type +.. c:function:: void _functionParam(int param) + + +.. c:member:: void __member = _member + + - :any:`_member` + - :c:member:`_member` + - :c:var:`_member` + - :c:data:`_member` + +.. c:member:: void __var = _var + + - :any:`_var` + - :c:member:`_var` + - :c:var:`_var` + - :c:data:`_var` + +.. c:member:: void __function = _function + + - :any:`_function` + - :c:func:`_function` + - :c:type:`_function` + +.. c:member:: void __macro = _macro + + - :any:`_macro` + - :c:macro:`_macro` + +.. c:type:: _struct __struct + struct _struct __structTagged + + - :any:`_struct` + - :c:struct:`_struct` + - :c:type:`_struct` + +.. c:type:: _union __union + union _union __unionTagged + + - :any:`_union` + - :c:union:`_union` + - :c:type:`_union` + +.. c:type:: _enum __enum + enum _enum __enumTagged + + - :any:`_enum` + - :c:enum:`_enum` + - :c:type:`_enum` + +.. c:member:: void __enumerator = _enumerator + + - :any:`_enumerator` + - :c:enumerator:`_enumerator` + +.. c:type:: _type __type + + - :any:`_type` + - :c:type:`_type` + +.. c:member:: void __functionParam = _functionParam.param + + - :any:`_functionParam.param` + - :c:member:`_functionParam.param` + - :c:var:`_functionParam.param` + - :c:data:`_functionParam.param` + +- :any:`_struct.@anon.i` +- :c:member:`_struct.@anon.i` +- :c:var:`_struct.@anon.i` +- :c:data:`_struct.@anon.i` +- :any:`_struct.i` +- :c:member:`_struct.i` +- :c:var:`_struct.i` +- :c:data:`_struct.i` diff --git a/tests/roots/test-domain-c/ids-vs-tags1.rst b/tests/roots/test-domain-c/ids-vs-tags1.rst new file mode 100644 index 00000000000..177722dcd40 --- /dev/null +++ b/tests/roots/test-domain-c/ids-vs-tags1.rst @@ -0,0 +1,27 @@ +.. c:namespace:: ids_vs_tags + +.. c:struct:: f_struct +.. c:type:: struct f_struct f_struct +.. c:union:: f_union +.. c:type:: union f_union f_union +.. c:enum:: f_enum +.. c:type:: enum f_enum f_enum + +- :c:struct:`f_struct` +- :c:struct:`struct f_struct` +- :c:type:`f_struct` +- :c:type:`struct f_struct` +- :any:`f_struct` +- :any:`struct f_struct` +- :c:union:`f_union` +- :c:union:`union f_union` +- :c:type:`f_union` +- :c:type:`union f_union` +- :any:`f_union` +- :any:`union f_union` +- :c:enum:`f_enum` +- :c:enum:`enum f_enum` +- :c:type:`f_enum` +- :c:type:`enum f_enum` +- :any:`f_enum` +- :any:`enum f_enum` diff --git a/tests/roots/test-domain-c/ids-vs-tags2.rst b/tests/roots/test-domain-c/ids-vs-tags2.rst new file mode 100644 index 00000000000..822ae583b52 --- /dev/null +++ b/tests/roots/test-domain-c/ids-vs-tags2.rst @@ -0,0 +1,22 @@ +.. c:namespace:: ids_vs_tags2 + +.. c:struct:: A + + .. c:union:: @B + + .. c:enum:: C + + .. c:enumerator:: D + +- :c:enumerator:`struct A.union @B.enum C.D` +- :c:enumerator:`A.union @B.enum C.D` +- :c:enumerator:`struct A.@B.enum C.D` +- :c:enumerator:`struct A.union @B.C.D` +- :c:enumerator:`A.@B.enum C.D` +- :c:enumerator:`A.union @B.C.D` +- :c:enumerator:`struct A.@B.C.D` +- :c:enumerator:`A.@B.C.D` +- :c:enumerator:`struct A.enum C.D` +- :c:enumerator:`A.enum C.D` +- :c:enumerator:`struct A.C.D` +- :c:enumerator:`A.C.D` diff --git a/tests/roots/test-domain-c/ids-vs-tags3.rst b/tests/roots/test-domain-c/ids-vs-tags3.rst new file mode 100644 index 00000000000..50d90506e22 --- /dev/null +++ b/tests/roots/test-domain-c/ids-vs-tags3.rst @@ -0,0 +1,19 @@ +.. c:namespace:: ids_vs_tags3 + +.. c:function:: void f1(int i) + +.. c:struct:: f1 + + .. c:var:: int i + + +.. c:struct:: f2 + + .. c:var:: int i + +.. c:function:: void f2(int i) + +- :c:var:`f1.i`, resolves to the function parameter +- :c:var:`struct f1.i`, resolves to struct f1.i +- :c:var:`f2.i`, resolves to the function parameter +- :c:var:`struct f2.i`, resolves to struct f2.i diff --git a/tests/roots/test-domain-c/index.rst b/tests/roots/test-domain-c/index.rst index 7e2c18be997..4f2a14d6012 100644 --- a/tests/roots/test-domain-c/index.rst +++ b/tests/roots/test-domain-c/index.rst @@ -1,3 +1,5 @@ +.. c:namespace:: index + test-domain-c ============= @@ -8,7 +10,7 @@ directives :rtype: int -.. c:function:: MyStruct hello2(char *name) +.. c:function:: struct MyStruct hello2(char *name) :rtype: MyStruct @@ -46,7 +48,7 @@ directives - :c:expr:`unsigned int` - :c:texpr:`unsigned int` -.. c:var:: A a +.. c:var:: struct A a - :c:expr:`a->b` - :c:texpr:`a->b` diff --git a/tests/roots/test-domain-c/semicolon.rst b/tests/roots/test-domain-c/semicolon.rst deleted file mode 100644 index 14ba177569d..00000000000 --- a/tests/roots/test-domain-c/semicolon.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. 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-c/wrong-tags.rst b/tests/roots/test-domain-c/wrong-tags.rst new file mode 100644 index 00000000000..5ca56b8a71e --- /dev/null +++ b/tests/roots/test-domain-c/wrong-tags.rst @@ -0,0 +1,14 @@ +.. c:namespace:: wrong_tag + +.. c:struct:: A + + .. c:var:: int i + +- :c:var:`A.i` +- :c:var:`union A.i` +- :c:var:`enum A.i` + +.. c:function:: void f1(union A a) +.. c:function:: void f2(enum A a) + +.. c:var:: int union A.j diff --git a/tests/test_build_html.py b/tests/test_build_html.py index fccf9cc6aa8..deb978b13c1 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -293,11 +293,11 @@ def test_html4_output(app, status, warning): (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), # C references (".//span[@class='pre']", 'CFunction()'), - (".//a[@href='#c.Sphinx_DoSomething']", ''), - (".//a[@href='#c.SphinxStruct.member']", ''), - (".//a[@href='#c.SPHINX_USE_PYTHON']", ''), - (".//a[@href='#c.SphinxType']", ''), - (".//a[@href='#c.sphinx_global']", ''), + (".//a[@href='#C2-Sphinx_DoSomething']", ''), + (".//a[@href='#C2-SphinxStruct.member']", ''), + (".//a[@href='#C2-SPHINX_USE_PYTHON']", ''), + (".//a[@href='#C2-SphinxType']", ''), + (".//a[@href='#C2-sphinx_global']", ''), # test global TOC created by toctree() (".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']", 'Testing object descriptions'), diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 2cfcf74faf4..55f2af23063 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -32,6 +32,21 @@ class Config: return ast +def parse_expression(expr, allowTypeExpr=False): + class Config: + c_id_attributes = ["id_attr"] + c_paren_attributes = ["paren_attr"] + parser = DefinitionParser(expr, location=None, config=Config()) + parser.allowFallbackExpressionParsing = False + if allowTypeExpr: + ast = parser.parse_expression() + else: + ast = parser._parse_expression() + parser.skip_ws() + parser.assert_end() + return ast + + def _check(name, input, idDict, output, key, asTextOutput): if key is None: key = name @@ -113,14 +128,8 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None): def test_expressions(): - def exprCheck(expr, output=None): - class Config: - c_id_attributes = ["id_attr"] - c_paren_attributes = ["paren_attr"] - parser = DefinitionParser(expr, location=None, config=Config()) - parser.allowFallbackExpressionParsing = False - ast = parser.parse_expression() - parser.assert_end() + def exprCheck(expr, output=None, allowTypeExpr=False): + ast = parse_expression(expr, allowTypeExpr) # first a simple check of the AST if output is None: output = expr @@ -141,14 +150,14 @@ class Config: raise DefinitionError("") # type expressions - exprCheck('int*') - exprCheck('int *const*') - exprCheck('int *volatile*') - exprCheck('int *restrict*') - exprCheck('int *(*)(double)') - exprCheck('const int*') - exprCheck('__int64') - exprCheck('unsigned __int64') + exprCheck('int*', allowTypeExpr=True) + exprCheck('int *const*', allowTypeExpr=True) + exprCheck('int *volatile*', allowTypeExpr=True) + exprCheck('int *restrict*', allowTypeExpr=True) + exprCheck('int *(*)(double)', allowTypeExpr=True) + exprCheck('const int*', allowTypeExpr=True) + exprCheck('__int64', allowTypeExpr=True) + exprCheck('unsigned __int64', allowTypeExpr=True) # actual expressions @@ -417,35 +426,65 @@ def test_function_definitions(): check('function', 'void f(int arr[const static volatile 42])', {1: 'f'}, output='void f(int arr[static volatile const 42])') + with pytest.raises(DefinitionError): + parse('function', 'void f(int for)') + def test_nested_name(): - check('struct', '{key}.A', {1: "A"}) - check('struct', '{key}.A.B', {1: "A.B"}) + check('struct', '{key}.A', {1: "A", 2: "-A"}) + check('struct', '{key}.A.B', {1: "A.B", 2: "A.-B"}) check('function', 'void f(.A a)', {1: "f"}) check('function', 'void f(.A.B a)', {1: "f"}) def test_struct_definitions(): - check('struct', '{key}A', {1: 'A'}) + check('struct', '{key}A', {1: 'A', 2: '-A'}) def test_union_definitions(): - check('union', '{key}A', {1: 'A'}) + check('union', '{key}A', {1: 'A', 2: '-A'}) def test_enum_definitions(): - check('enum', '{key}A', {1: 'A'}) + check('enum', '{key}A', {1: 'A', 2: '-A'}) check('enumerator', '{key}A', {1: 'A'}) check('enumerator', '{key}A = 42', {1: 'A'}) def test_anon_definitions(): - check('struct', '@a', {1: "@a"}, asTextOutput='struct [anonymous]') - check('union', '@a', {1: "@a"}, asTextOutput='union [anonymous]') - check('enum', '@a', {1: "@a"}, asTextOutput='enum [anonymous]') - check('struct', '@1', {1: "@1"}, asTextOutput='struct [anonymous]') - check('struct', '@a.A', {1: "@a.A"}, asTextOutput='struct [anonymous].A') + check('struct', '@a', {1: "@a", 2: "-@a"}, asTextOutput='struct [anonymous]') + check('union', '@a', {1: "@a", 2: "-@a"}, asTextOutput='union [anonymous]') + check('enum', '@a', {1: "@a", 2: "-@a"}, asTextOutput='enum [anonymous]') + check('struct', '@1', {1: "@1", 2: "-@1"}, asTextOutput='struct [anonymous]') + check('struct', '@a.A', {1: "@a.A", 2: "@a.-A"}, asTextOutput='struct [anonymous].A') + + +def test_tagged_names(): + for key in ('struct', 'union', 'enum'): + with pytest.raises(DefinitionError): + parse('member', "int {} A".format(key)) + with pytest.raises(DefinitionError): + parse('function', "int {} A()".format(key)) + with pytest.raises(DefinitionError): + parse('function', "int A(int {} a)".format(key)) + for objType in ('macro', 'struct', 'union', 'enum', 'enumerator'): + with pytest.raises(DefinitionError): + parse(objType, '{} A'.format(key)) + # type + with pytest.raises(DefinitionError): + parse('type', '{} A'.format(key)) + check('type', '{key1}{key2} A A'.format(key2=key, key1="{key}"), idDict={1: 'A'}, key='typedef') + # TODO: namespace, namespace-push, namespace-pop + # TODO: alias + # primary expression + for expr in ('{} a.b', 'a.{} b.c', 'a.{} b'): + with pytest.raises(DefinitionError): + parse_expression(expr.format(key)) + # postfix expression + for expr in ('a->{} b->c', 'a->{} b'): + with pytest.raises(DefinitionError): + parse_expression(expr.format(key)) def test_initializers(): @@ -523,8 +562,15 @@ def test_attributes(): # raise DefinitionError("") +def split_warnigns(warning): + ws = warning.getvalue().split("\n") + assert len(ws) >= 1 + assert ws[-1] == "" + return ws[:-1] + + def filter_warnings(warning, file): - lines = warning.getvalue().split("\n") + lines = split_warnigns(warning) res = [l for l in lines if "domain-c" in l and "{}.rst".format(file) in l and "WARNING: document isn't included in any toctree" not in l] print("Filtered warnings for file '{}':".format(file)) @@ -579,9 +625,155 @@ def test_build_domain_c_anon_dup_decl(app, status, warning): @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) -def test_build_domain_c_semicolon(app, status, warning): +def test_ids_vs_tags0(app, status, warning): + # this test is essentially the base case for the intersphinx tests, + # where both the primary declarations and the references are in one project + app.builder.build_all() + ws = filter_warnings(warning, "ids-vs-tags0") + assert len(ws) == 3 + msg = "WARNING: c:identifier reference has incorrect tagging:" + msg += " Full reference name is '{}'." + msg += " Full found name is '{}'." + assert msg.format("_struct", "struct _struct") in ws[0] + assert msg.format("_union", "union _union") in ws[1] + assert msg.format("_enum", "enum _enum") in ws[2] + + +@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) +def test_ids_vs_tags1(app, warning): + app.builder.build_all() + ws = filter_warnings(warning, "ids-vs-tags1") + assert len(ws) == 0 + t = (app.outdir / "ids-vs-tags1.html").read_text() + lis = [l for l in t.split('\n') if l.startswith("