diff options
Diffstat (limited to 'sphinx/domains')
-rw-r--r-- | sphinx/domains/__init__.py | 406 | ||||
-rw-r--r-- | sphinx/domains/c.py | 3906 | ||||
-rw-r--r-- | sphinx/domains/changeset.py | 161 | ||||
-rw-r--r-- | sphinx/domains/citation.py | 154 | ||||
-rw-r--r-- | sphinx/domains/cpp.py | 8233 | ||||
-rw-r--r-- | sphinx/domains/index.py | 126 | ||||
-rw-r--r-- | sphinx/domains/javascript.py | 508 | ||||
-rw-r--r-- | sphinx/domains/math.py | 152 | ||||
-rw-r--r-- | sphinx/domains/python.py | 1769 | ||||
-rw-r--r-- | sphinx/domains/rst.py | 299 | ||||
-rw-r--r-- | sphinx/domains/std.py | 1123 |
11 files changed, 16837 insertions, 0 deletions
diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py new file mode 100644 index 0000000..7c11220 --- /dev/null +++ b/sphinx/domains/__init__.py @@ -0,0 +1,406 @@ +"""Support for domains. + +Domains are groupings of description directives +and roles describing e.g. constructs of one programming language. +""" + +from __future__ import annotations + +import copy +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, cast + +from docutils.nodes import Element, Node, system_message + +from sphinx.errors import SphinxError +from sphinx.locale import _ + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from docutils import nodes + from docutils.parsers.rst import Directive + from docutils.parsers.rst.states import Inliner + + from sphinx.addnodes import pending_xref + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.roles import XRefRole + from sphinx.util.typing import RoleFunction + + +class ObjType: + """ + An ObjType is the description for a type of object that a domain can + document. In the object_types attribute of Domain subclasses, object type + names are mapped to instances of this class. + + Constructor arguments: + + - *lname*: localized name of the type (do not include domain name) + - *roles*: all the roles that can refer to an object of this type + - *attrs*: object attributes -- currently only "searchprio" is known, + which defines the object's priority in the full-text search index, + see :meth:`Domain.get_objects()`. + """ + + known_attrs = { + 'searchprio': 1, + } + + def __init__(self, lname: str, *roles: Any, **attrs: Any) -> None: + self.lname = lname + self.roles: tuple = roles + self.attrs: dict = self.known_attrs.copy() + self.attrs.update(attrs) + + +class IndexEntry(NamedTuple): + name: str + subtype: int + docname: str + anchor: str + extra: str + qualifier: str + descr: str + + +class Index(ABC): + """ + An Index is the description for a domain-specific index. To add an index to + a domain, subclass Index, overriding the three name attributes: + + * `name` is an identifier used for generating file names. + It is also used for a hyperlink target for the index. Therefore, users can + refer the index page using ``ref`` role and a string which is combined + domain name and ``name`` attribute (ex. ``:ref:`py-modindex```). + * `localname` is the section title for the index. + * `shortname` is a short name for the index, for use in the relation bar in + HTML output. Can be empty to disable entries in the relation bar. + + and providing a :meth:`generate()` method. Then, add the index class to + your domain's `indices` list. Extensions can add indices to existing + domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. + + .. versionchanged:: 3.0 + + Index pages can be referred by domain name and index name via + :rst:role:`ref` role. + """ + + name: str + localname: str + shortname: str | None = None + + def __init__(self, domain: Domain) -> None: + if self.name is None or self.localname is None: + raise SphinxError('Index subclass %s has no valid name or localname' + % self.__class__.__name__) + self.domain = domain + + @abstractmethod + def generate(self, docnames: Iterable[str] | None = None, + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + """Get entries for the index. + + If ``docnames`` is given, restrict to entries referring to these + docnames. + + The return value is a tuple of ``(content, collapse)``: + + ``collapse`` + A boolean that determines if sub-entries should start collapsed (for + output formats that support collapsing sub-entries). + + ``content``: + A sequence of ``(letter, entries)`` tuples, where ``letter`` is the + "heading" for the given ``entries``, usually the starting letter, and + ``entries`` is a sequence of single entries. Each entry is a sequence + ``[name, subtype, docname, anchor, extra, qualifier, descr]``. The + items in this sequence have the following meaning: + + ``name`` + The name of the index entry to be displayed. + + ``subtype`` + The sub-entry related type. One of: + + ``0`` + A normal entry. + ``1`` + An entry with sub-entries. + ``2`` + A sub-entry. + + ``docname`` + *docname* where the entry is located. + + ``anchor`` + Anchor for the entry within ``docname`` + + ``extra`` + Extra info for the entry. + + ``qualifier`` + Qualifier for the description. + + ``descr`` + Description for the entry. + + Qualifier and description are not rendered for some output formats such + as LaTeX. + """ + raise NotImplementedError + + +TitleGetter = Callable[[Node], Optional[str]] + + +class Domain: + """ + A Domain is meant to be a group of "object" description directives for + objects of a similar nature, and corresponding roles to create references to + them. Examples would be Python modules, classes, functions etc., elements + of a templating language, Sphinx roles and directives, etc. + + Each domain has a separate storage for information about existing objects + and how to reference them in `self.data`, which must be a dictionary. It + also must implement several functions that expose the object information in + a uniform way to parts of Sphinx that allow the user to reference or search + for objects in a domain-agnostic way. + + About `self.data`: since all object and cross-referencing information is + stored on a BuildEnvironment instance, the `domain.data` object is also + stored in the `env.domaindata` dict under the key `domain.name`. Before the + build process starts, every active domain is instantiated and given the + environment object; the `domaindata` dict must then either be nonexistent or + a dictionary whose 'version' key is equal to the domain class' + :attr:`data_version` attribute. Otherwise, `OSError` is raised and the + pickled environment is discarded. + """ + + #: domain name: should be short, but unique + name = '' + #: domain label: longer, more descriptive (used in messages) + label = '' + #: type (usually directive) name -> ObjType instance + object_types: dict[str, ObjType] = {} + #: directive name -> directive class + directives: dict[str, type[Directive]] = {} + #: role name -> role callable + roles: dict[str, RoleFunction | XRefRole] = {} + #: a list of Index subclasses + indices: list[type[Index]] = [] + #: role name -> a warning message if reference is missing + dangling_warnings: dict[str, str] = {} + #: node_class -> (enum_node_type, title_getter) + enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = {} + #: data value for a fresh environment + initial_data: dict = {} + #: data value + data: dict + #: data version, bump this when the format of `self.data` changes + data_version = 0 + + def __init__(self, env: BuildEnvironment) -> None: + self.env: BuildEnvironment = env + self._role_cache: dict[str, Callable] = {} + self._directive_cache: dict[str, Callable] = {} + self._role2type: dict[str, list[str]] = {} + self._type2role: dict[str, str] = {} + + # convert class variables to instance one (to enhance through API) + self.object_types = dict(self.object_types) + self.directives = dict(self.directives) + self.roles = dict(self.roles) + self.indices = list(self.indices) + + if self.name not in env.domaindata: + assert isinstance(self.initial_data, dict) + new_data = copy.deepcopy(self.initial_data) + new_data['version'] = self.data_version + self.data = env.domaindata[self.name] = new_data + else: + self.data = env.domaindata[self.name] + if self.data['version'] != self.data_version: + raise OSError('data of %r domain out of date' % self.label) + for name, obj in self.object_types.items(): + for rolename in obj.roles: + self._role2type.setdefault(rolename, []).append(name) + self._type2role[name] = obj.roles[0] if obj.roles else '' + self.objtypes_for_role = self._role2type.get + self.role_for_objtype = self._type2role.get + + def setup(self) -> None: + """Set up domain object.""" + from sphinx.domains.std import StandardDomain + + # Add special hyperlink target for index pages (ex. py-modindex) + std = cast(StandardDomain, self.env.get_domain('std')) + for index in self.indices: + if index.name and index.localname: + docname = f"{self.name}-{index.name}" + std.note_hyperlink_target(docname, docname, '', index.localname) + + def add_object_type(self, name: str, objtype: ObjType) -> None: + """Add an object type.""" + self.object_types[name] = objtype + if objtype.roles: + self._type2role[name] = objtype.roles[0] + else: + self._type2role[name] = '' + + for role in objtype.roles: + self._role2type.setdefault(role, []).append(name) + + def role(self, name: str) -> RoleFunction | None: + """Return a role adapter function that always gives the registered + role its full name ('domain:name') as the first argument. + """ + if name in self._role_cache: + return self._role_cache[name] + if name not in self.roles: + return None + fullname = f'{self.name}:{name}' + + def role_adapter(typ: str, rawtext: str, text: str, lineno: int, + inliner: Inliner, options: dict | None = None, + content: Sequence[str] = (), + ) -> tuple[list[Node], list[system_message]]: + return self.roles[name](fullname, rawtext, text, lineno, + inliner, options or {}, content) + self._role_cache[name] = role_adapter + return role_adapter + + def directive(self, name: str) -> Callable | None: + """Return a directive adapter class that always gives the registered + directive its full name ('domain:name') as ``self.name``. + """ + if name in self._directive_cache: + return self._directive_cache[name] + if name not in self.directives: + return None + fullname = f'{self.name}:{name}' + BaseDirective = self.directives[name] + + class DirectiveAdapter(BaseDirective): # type: ignore[valid-type,misc] + def run(self) -> list[Node]: + self.name = fullname + return super().run() + self._directive_cache[name] = DirectiveAdapter + return DirectiveAdapter + + # methods that should be overwritten + + def clear_doc(self, docname: str) -> None: + """Remove traces of a document in the domain-specific inventories.""" + pass + + def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None: + """Merge in data regarding *docnames* from a different domaindata + inventory (coming from a subprocess in parallel builds). + """ + raise NotImplementedError('merge_domaindata must be implemented in %s ' + 'to be able to do parallel builds!' % + self.__class__) + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + """Process a document after it is read by the environment.""" + pass + + def check_consistency(self) -> None: + """Do consistency checks (**experimental**).""" + pass + + def process_field_xref(self, pnode: pending_xref) -> None: + """Process a pending xref created in a doc field. + For example, attach information about the current scope. + """ + pass + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + """Resolve the pending_xref *node* with the given *typ* and *target*. + + This method should return a new node, to replace the xref node, + containing the *contnode* which is the markup content of the + cross-reference. + + If no resolution can be found, None can be returned; the xref node will + then given to the :event:`missing-reference` event, and if that yields no + resolution, replaced by *contnode*. + + The method can also raise :exc:`sphinx.environment.NoUri` to suppress + the :event:`missing-reference` event being emitted. + """ + pass + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + """Resolve the pending_xref *node* with the given *target*. + + The reference comes from an "any" or similar role, which means that we + don't know the type. Otherwise, the arguments are the same as for + :meth:`resolve_xref`. + + The method must return a list (potentially empty) of tuples + ``('domain:role', newnode)``, where ``'domain:role'`` is the name of a + role that could have created the same reference, e.g. ``'py:func'``. + ``newnode`` is what :meth:`resolve_xref` would return. + + .. versionadded:: 1.3 + """ + raise NotImplementedError + + def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]: + """Return an iterable of "object descriptions". + + Object descriptions are tuples with six items: + + ``name`` + Fully qualified name. + + ``dispname`` + Name to display when searching/linking. + + ``type`` + Object type, a key in ``self.object_types``. + + ``docname`` + The document where it is to be found. + + ``anchor`` + The anchor name for the object. + + ``priority`` + How "important" the object is (determines placement in search + results). One of: + + ``1`` + Default priority (placed before full-text matches). + ``0`` + Object is important (placed before default-priority objects). + ``2`` + Object is unimportant (placed after full-text matches). + ``-1`` + Object should not show up in search at all. + """ + return [] + + def get_type_name(self, type: ObjType, primary: bool = False) -> str: + """Return full name for given ObjType.""" + if primary: + return type.lname + return _('%s %s') % (self.label, type.lname) + + def get_enumerable_node_type(self, node: Node) -> str | None: + """Get type of enumerable nodes (experimental).""" + enum_node_type, _ = self.enumerable_nodes.get(node.__class__, (None, None)) + return enum_node_type + + def get_full_qualified_name(self, node: Element) -> str | None: + """Return full qualified name for given node.""" + pass diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py new file mode 100644 index 0000000..9444c6b --- /dev/null +++ b/sphinx/domains/c.py @@ -0,0 +1,3906 @@ +"""The C language domain.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union, cast + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, ObjType +from sphinx.locale import _, __ +from sphinx.roles import SphinxRole, XRefRole +from sphinx.transforms import SphinxTransform +from sphinx.transforms.post_transforms import ReferencesResolver +from sphinx.util import logging +from sphinx.util.cfamily import ( + ASTAttributeList, + ASTBaseBase, + ASTBaseParenExprList, + BaseParser, + DefinitionError, + NoOldIdError, + StringifyTransform, + UnsupportedMultiCharacterCharLiteral, + anon_identifier_re, + binary_literal_re, + char_literal_re, + float_literal_re, + float_literal_suffix_re, + hex_literal_re, + identifier_re, + integer_literal_re, + integers_literal_suffix_re, + octal_literal_re, + verify_description_mode, +) +from sphinx.util.docfields import Field, GroupedField, TypedField +from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import make_refnode + +if TYPE_CHECKING: + from collections.abc import Generator, Iterator + + from docutils.nodes import Element, Node, TextElement, system_message + + from sphinx.addnodes import pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + +logger = logging.getLogger(__name__) +T = TypeVar('T') + +DeclarationType = Union[ + "ASTStruct", "ASTUnion", "ASTEnum", "ASTEnumerator", + "ASTType", "ASTTypeWithInit", "ASTMacro", +] + +# https://en.cppreference.com/w/c/keyword +_keywords = [ + 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', + 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if', 'inline', 'int', 'long', + 'register', 'restrict', 'return', 'short', 'signed', 'sizeof', 'static', 'struct', + 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', + '_Alignas', '_Alignof', '_Atomic', '_Bool', '_Complex', + '_Decimal32', '_Decimal64', '_Decimal128', + '_Generic', '_Imaginary', '_Noreturn', '_Static_assert', '_Thread_local', +] +# These are only keyword'y when the corresponding headers are included. +# They are used as default value for c_extra_keywords. +_macroKeywords = [ + 'alignas', 'alignof', 'bool', 'complex', 'imaginary', 'noreturn', 'static_assert', + 'thread_local', +] + +# these are ordered by precedence +_expression_bin_ops = [ + ['||', 'or'], + ['&&', 'and'], + ['|', 'bitor'], + ['^', 'xor'], + ['&', 'bitand'], + ['==', '!=', 'not_eq'], + ['<=', '>=', '<', '>'], + ['<<', '>>'], + ['+', '-'], + ['*', '/', '%'], + ['.*', '->*'], +] +_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "not", "~", "compl"] +_expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", + ">>=", "<<=", "&=", "and_eq", "^=", "xor_eq", "|=", "or_eq"] + +_max_id = 1 +_id_prefix = [None, 'c.', 'Cv2.'] +# Ids are used in lookup keys which are used across pickled files, +# so when _max_id changes, make sure to update the ENV_VERSION. + +_string_re = re.compile(r"[LuU8]?('([^'\\]*(?:\\.[^'\\]*)*)'" + r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) + +# bool, complex, and imaginary are macro "keywords", so they are handled separately +_simple_type_specifiers_re = re.compile(r""" + \b( + void|_Bool + |signed|unsigned + |short|long + |char + |int + |__uint128|__int128 + |__int(8|16|32|64|128) # extension + |float|double + |_Decimal(32|64|128) + |_Complex|_Imaginary + |__float80|_Float64x|__float128|_Float128|__ibm128 # extension + |__fp16 # extension + |_Sat|_Fract|fract|_Accum|accum # extension + )\b +""", re.VERBOSE) + + +class _DuplicateSymbolError(Exception): + def __init__(self, symbol: Symbol, declaration: ASTDeclaration) -> None: + assert symbol + assert declaration + self.symbol = symbol + self.declaration = declaration + + def __str__(self) -> str: + return "Internal C duplicate symbol error:\n%s" % self.symbol.dump(0) + + +class ASTBase(ASTBaseBase): + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + +# Names +################################################################################ + +class ASTIdentifier(ASTBaseBase): + def __init__(self, identifier: str) -> None: + 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 + + def is_anon(self) -> bool: + return self.identifier[0] == '@' + + # and this is where we finally make a difference between __str__ and the display string + + def __str__(self) -> str: + return self.identifier + + def get_display_string(self) -> str: + return "[anonymous]" if self.is_anon() else self.identifier + + 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.is_anon(): + node = addnodes.desc_sig_name(text="[anonymous]") + else: + node = addnodes.desc_sig_name(self.identifier, self.identifier) + if mode == 'markType': + targetText = prefix + self.identifier + pnode = addnodes.pending_xref('', refdomain='c', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + pnode['c:parent_key'] = symbol.get_lookup_key() + pnode += node + signode += pnode + elif mode == 'lastIsName': + nameNode = addnodes.desc_name() + nameNode += node + signode += nameNode + elif mode == 'noneIsName': + signode += node + else: + raise Exception('Unknown description mode: %s' % mode) + + +class ASTNestedName(ASTBase): + def __init__(self, names: list[ASTIdentifier], rooted: bool) -> None: + assert len(names) > 0 + self.names = names + self.rooted = rooted + + @property + def name(self) -> ASTNestedName: + return self + + def get_id(self, version: int) -> str: + return '.'.join(str(n) for n in self.names) + + def _stringify(self, transform: StringifyTransform) -> str: + res = '.'.join(transform(n) for n in self.names) + if self.rooted: + return '.' + res + else: + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + # just print the name part, with template args, not template params + if mode == 'noneIsName': + if self.rooted: + unreachable = "Can this happen?" + raise AssertionError(unreachable) # TODO + signode += nodes.Text('.') + for i in range(len(self.names)): + if i != 0: + unreachable = "Can this happen?" + raise AssertionError(unreachable) # TODO + signode += nodes.Text('.') + n = self.names[i] + n.describe_signature(signode, mode, env, '', symbol) + elif mode == 'param': + assert not self.rooted, str(self) + assert len(self.names) == 1 + self.names[0].describe_signature(signode, 'noneIsName', env, '', symbol) + elif mode in ('markType', 'lastIsName', 'markName'): + # Each element should be a pending xref targeting the complete + # prefix. + prefix = '' + first = True + names = self.names[:-1] if mode == 'lastIsName' else self.names + # If lastIsName, then wrap all of the prefix in a desc_addname, + # else append directly to signode. + # TODO: also for C? + # NOTE: Breathe previously relied on the prefix being in the desc_addname node, + # so it can remove it in inner declarations. + dest = signode + if mode == 'lastIsName': + dest = addnodes.desc_addname() + if self.rooted: + prefix += '.' + if mode == 'lastIsName' and len(names) == 0: + signode += addnodes.desc_sig_punctuation('.', '.') + else: + dest += addnodes.desc_sig_punctuation('.', '.') + for i in range(len(names)): + ident = names[i] + if not first: + dest += addnodes.desc_sig_punctuation('.', '.') + prefix += '.' + first = False + txt_ident = str(ident) + if txt_ident != '': + ident.describe_signature(dest, 'markType', env, prefix, symbol) + prefix += txt_ident + if mode == 'lastIsName': + if len(self.names) > 1: + dest += addnodes.desc_sig_punctuation('.', '.') + signode += dest + self.names[-1].describe_signature(signode, mode, env, '', symbol) + else: + raise Exception('Unknown description mode: %s' % mode) + + +################################################################################ +# Expressions +################################################################################ + +class ASTExpression(ASTBase): + pass + + +# Primary expressions +################################################################################ + +class ASTLiteral(ASTExpression): + pass + + +class ASTBooleanLiteral(ASTLiteral): + def __init__(self, value: bool) -> None: + self.value = value + + def _stringify(self, transform: StringifyTransform) -> str: + if self.value: + return 'true' + else: + return 'false' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + txt = str(self) + signode += addnodes.desc_sig_keyword(txt, txt) + + +class ASTNumberLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + txt = str(self) + signode += addnodes.desc_sig_literal_number(txt, txt) + + +class ASTCharLiteral(ASTLiteral): + def __init__(self, prefix: str, data: str) -> None: + self.prefix = prefix # may be None when no prefix + self.data = data + decoded = data.encode().decode('unicode-escape') + if len(decoded) == 1: + self.value = ord(decoded) + else: + raise UnsupportedMultiCharacterCharLiteral(decoded) + + def _stringify(self, transform: StringifyTransform) -> str: + if self.prefix is None: + return "'" + self.data + "'" + else: + return self.prefix + "'" + self.data + "'" + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + txt = str(self) + signode += addnodes.desc_sig_literal_char(txt, txt) + + +class ASTStringLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + txt = str(self) + signode += addnodes.desc_sig_literal_string(txt, txt) + + +class ASTIdExpression(ASTExpression): + def __init__(self, name: ASTNestedName): + # note: this class is basically to cast a nested name as an expression + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def get_id(self, version: int) -> str: + return self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.name.describe_signature(signode, mode, env, symbol) + + +class ASTParenExpr(ASTExpression): + def __init__(self, expr): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '(' + transform(self.expr) + ')' + + def get_id(self, version: int) -> str: + return self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +# Postfix expressions +################################################################################ + +class ASTPostfixOp(ASTBase): + pass + + +class ASTPostfixCallExpr(ASTPostfixOp): + def __init__(self, lst: ASTParenExprList | ASTBracedInitList) -> None: + self.lst = lst + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.lst) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.lst.describe_signature(signode, mode, env, symbol) + + +class ASTPostfixArray(ASTPostfixOp): + def __init__(self, expr: ASTExpression) -> None: + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '[' + transform(self.expr) + ']' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('[', '[') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(']', ']') + + +class ASTPostfixInc(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '++' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_operator('++', '++') + + +class ASTPostfixDec(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '--' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_operator('--', '--') + + +class ASTPostfixMemberOfPointer(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 += addnodes.desc_sig_operator('->', '->') + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixExpr(ASTExpression): + def __init__(self, prefix: ASTExpression, postFixes: list[ASTPostfixOp]): + self.prefix = prefix + self.postFixes = postFixes + + def _stringify(self, transform: StringifyTransform) -> str: + res = [transform(self.prefix)] + for p in self.postFixes: + res.append(transform(p)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.prefix.describe_signature(signode, mode, env, symbol) + for p in self.postFixes: + p.describe_signature(signode, mode, env, symbol) + + +# Unary expressions +################################################################################ + +class ASTUnaryOpExpr(ASTExpression): + def __init__(self, op: str, expr: ASTExpression): + self.op = op + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + if self.op[0] in 'cn': + return self.op + " " + transform(self.expr) + else: + return self.op + transform(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.op[0] in 'cn': + signode += addnodes.desc_sig_keyword(self.op, self.op) + signode += addnodes.desc_sig_space() + else: + signode += addnodes.desc_sig_operator(self.op, self.op) + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTSizeofType(ASTExpression): + def __init__(self, typ): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof(" + transform(self.typ) + ")" + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('sizeof', 'sizeof') + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTSizeofExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof " + transform(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('sizeof', 'sizeof') + signode += addnodes.desc_sig_space() + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTAlignofExpr(ASTExpression): + def __init__(self, typ: ASTType): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "alignof(" + transform(self.typ) + ")" + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('alignof', 'alignof') + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +# Other expressions +################################################################################ + +class ASTCastExpr(ASTExpression): + def __init__(self, typ: ASTType, expr: ASTExpression): + self.typ = typ + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.typ)) + res.append(')') + res.append(transform(self.expr)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTBinOpExpr(ASTBase): + def __init__(self, exprs: list[ASTExpression], ops: list[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode += addnodes.desc_sig_space() + op = self.ops[i - 1] + if ord(op[0]) >= ord('a') and ord(op[0]) <= ord('z'): + signode += addnodes.desc_sig_keyword(op, op) + else: + signode += addnodes.desc_sig_operator(op, op) + signode += addnodes.desc_sig_space() + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTAssignmentExpr(ASTExpression): + def __init__(self, exprs: list[ASTExpression], ops: list[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode += addnodes.desc_sig_space() + op = self.ops[i - 1] + if ord(op[0]) >= ord('a') and ord(op[0]) <= ord('z'): + signode += addnodes.desc_sig_keyword(op, op) + else: + signode += addnodes.desc_sig_operator(op, op) + signode += addnodes.desc_sig_space() + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTFallbackExpr(ASTExpression): + def __init__(self, expr: str): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return self.expr + + def get_id(self, version: int) -> str: + return str(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += nodes.literal(self.expr, self.expr) + + +################################################################################ +# Types +################################################################################ + +class ASTTrailingTypeSpec(ASTBase): + pass + + +class ASTTrailingTypeSpecFundamental(ASTTrailingTypeSpec): + def __init__(self, names: list[str]) -> None: + assert len(names) != 0 + self.names = names + + def _stringify(self, transform: StringifyTransform) -> str: + return ' '.join(self.names) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + first = True + for n in self.names: + if not first: + signode += addnodes.desc_sig_space() + else: + first = False + signode += addnodes.desc_sig_keyword_type(n, n) + + +class ASTTrailingTypeSpecName(ASTTrailingTypeSpec): + def __init__(self, prefix: str, nestedName: ASTNestedName) -> None: + self.prefix = prefix + self.nestedName = nestedName + + @property + 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) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.prefix: + signode += addnodes.desc_sig_keyword(self.prefix, self.prefix) + signode += addnodes.desc_sig_space() + self.nestedName.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTFunctionParameter(ASTBase): + def __init__(self, arg: ASTTypeWithInit | None, ellipsis: bool = False) -> None: + self.arg = arg + self.ellipsis = ellipsis + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + + def _stringify(self, transform: StringifyTransform) -> str: + if self.ellipsis: + return '...' + else: + return transform(self.arg) + + def describe_signature(self, signode: Any, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.ellipsis: + signode += addnodes.desc_sig_punctuation('...', '...') + else: + self.arg.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTParameters(ASTBase): + def __init__(self, args: list[ASTFunctionParameter], attrs: ASTAttributeList) -> None: + self.args = args + self.attrs = attrs + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.args + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append('(') + first = True + for a in self.args: + if not first: + res.append(', ') + first = False + res.append(str(a)) + res.append(')') + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.attrs)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + multi_line_parameter_list = False + test_node: Element = signode + while test_node.parent: + if not isinstance(test_node, addnodes.desc_signature): + test_node = test_node.parent + continue + multi_line_parameter_list = test_node.get('multi_line_parameter_list', False) + break + + # only use the desc_parameterlist for the outer list, not for inner lists + if mode == 'lastIsName': + paramlist = addnodes.desc_parameterlist() + paramlist['multi_line_parameter_list'] = multi_line_parameter_list + for arg in self.args: + param = addnodes.desc_parameter('', '', noemph=True) + arg.describe_signature(param, 'param', env, symbol=symbol) + paramlist += param + signode += paramlist + else: + signode += addnodes.desc_sig_punctuation('(', '(') + first = True + for arg in self.args: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + first = False + arg.describe_signature(signode, 'markType', env, symbol=symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.attrs.describe_signature(signode) + + +class ASTDeclSpecsSimple(ASTBaseBase): + def __init__(self, storage: str, threadLocal: str, inline: bool, + restrict: bool, volatile: bool, const: bool, attrs: ASTAttributeList) -> None: + self.storage = storage + self.threadLocal = threadLocal + self.inline = inline + self.restrict = restrict + self.volatile = volatile + self.const = const + self.attrs = attrs + + def mergeWith(self, other: ASTDeclSpecsSimple) -> ASTDeclSpecsSimple: + if not other: + return self + return ASTDeclSpecsSimple(self.storage or other.storage, + self.threadLocal or other.threadLocal, + self.inline or other.inline, + self.volatile or other.volatile, + self.const or other.const, + self.restrict or other.restrict, + self.attrs + other.attrs) + + def _stringify(self, transform: StringifyTransform) -> str: + res: list[str] = [] + if len(self.attrs) != 0: + res.append(transform(self.attrs)) + if self.storage: + res.append(self.storage) + if self.threadLocal: + res.append(self.threadLocal) + if self.inline: + res.append('inline') + if self.restrict: + res.append('restrict') + if self.volatile: + res.append('volatile') + if self.const: + res.append('const') + return ' '.join(res) + + def describe_signature(self, modifiers: list[Node]) -> None: + def _add(modifiers: list[Node], text: str) -> None: + if len(modifiers) != 0: + modifiers.append(addnodes.desc_sig_space()) + modifiers.append(addnodes.desc_sig_keyword(text, text)) + + if len(modifiers) != 0 and len(self.attrs) != 0: + modifiers.append(addnodes.desc_sig_space()) + tempNode = nodes.TextElement() + self.attrs.describe_signature(tempNode) + modifiers.extend(tempNode.children) + if self.storage: + _add(modifiers, self.storage) + if self.threadLocal: + _add(modifiers, self.threadLocal) + if self.inline: + _add(modifiers, 'inline') + if self.restrict: + _add(modifiers, 'restrict') + if self.volatile: + _add(modifiers, 'volatile') + if self.const: + _add(modifiers, 'const') + + +class ASTDeclSpecs(ASTBase): + def __init__(self, outer: str, + leftSpecs: ASTDeclSpecsSimple, + rightSpecs: ASTDeclSpecsSimple, + trailing: ASTTrailingTypeSpec) -> None: + # leftSpecs and rightSpecs are used for output + # allSpecs are used for id generation TODO: remove? + self.outer = outer + self.leftSpecs = leftSpecs + self.rightSpecs = rightSpecs + self.allSpecs = self.leftSpecs.mergeWith(self.rightSpecs) + self.trailingTypeSpec = trailing + + def _stringify(self, transform: StringifyTransform) -> str: + res: list[str] = [] + l = transform(self.leftSpecs) + if len(l) > 0: + res.append(l) + if self.trailingTypeSpec: + if len(res) > 0: + res.append(" ") + res.append(transform(self.trailingTypeSpec)) + r = str(self.rightSpecs) + if len(r) > 0: + if len(res) > 0: + res.append(" ") + res.append(r) + return "".join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + modifiers: list[Node] = [] + + self.leftSpecs.describe_signature(modifiers) + + for m in modifiers: + signode += m + if self.trailingTypeSpec: + if len(modifiers) > 0: + signode += addnodes.desc_sig_space() + self.trailingTypeSpec.describe_signature(signode, mode, env, + symbol=symbol) + modifiers = [] + self.rightSpecs.describe_signature(modifiers) + if len(modifiers) > 0: + signode += addnodes.desc_sig_space() + for m in modifiers: + signode += m + + +# Declarator +################################################################################ + +class ASTArray(ASTBase): + def __init__(self, static: bool, const: bool, volatile: bool, restrict: bool, + vla: bool, size: ASTExpression): + self.static = static + self.const = const + self.volatile = volatile + self.restrict = restrict + self.vla = vla + self.size = size + if vla: + assert size is None + if size is not None: + assert not vla + + def _stringify(self, transform: StringifyTransform) -> str: + el = [] + if self.static: + el.append('static') + if self.restrict: + el.append('restrict') + if self.volatile: + el.append('volatile') + if self.const: + el.append('const') + if self.vla: + return '[' + ' '.join(el) + '*]' + elif self.size: + el.append(transform(self.size)) + return '[' + ' '.join(el) + ']' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('[', '[') + addSpace = False + + def _add(signode: TextElement, text: str) -> bool: + if addSpace: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_keyword(text, text) + return True + + if self.static: + addSpace = _add(signode, 'static') + if self.restrict: + addSpace = _add(signode, 'restrict') + if self.volatile: + addSpace = _add(signode, 'volatile') + if self.const: + addSpace = _add(signode, 'const') + if self.vla: + signode += addnodes.desc_sig_punctuation('*', '*') + elif self.size: + if addSpace: + signode += addnodes.desc_sig_space() + self.size.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation(']', ']') + + +class ASTDeclarator(ASTBase): + @property + def name(self) -> ASTNestedName: + raise NotImplementedError(repr(self)) + + @property + def function_params(self) -> list[ASTFunctionParameter]: + raise NotImplementedError(repr(self)) + + def require_space_after_declSpecs(self) -> bool: + raise NotImplementedError(repr(self)) + + +class ASTDeclaratorNameParam(ASTDeclarator): + def __init__(self, declId: ASTNestedName, + arrayOps: list[ASTArray], param: ASTParameters) -> None: + self.declId = declId + self.arrayOps = arrayOps + self.param = param + + @property + def name(self) -> ASTNestedName: + return self.declId + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.param.function_params + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + for op in self.arrayOps: + res.append(transform(op)) + if self.param: + res.append(transform(self.param)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + for op in self.arrayOps: + op.describe_signature(signode, mode, env, symbol) + if self.param: + self.param.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorNameBitField(ASTDeclarator): + def __init__(self, declId: ASTNestedName, size: ASTExpression): + self.declId = declId + self.size = size + + @property + def name(self) -> ASTNestedName: + return self.declId + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + res.append(" : ") + res.append(transform(self.size)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation(':', ':') + signode += addnodes.desc_sig_space() + self.size.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorPtr(ASTDeclarator): + def __init__(self, next: ASTDeclarator, restrict: bool, volatile: bool, const: bool, + attrs: ASTAttributeList) -> None: + assert next + self.next = next + self.restrict = restrict + self.volatile = volatile + self.const = const + self.attrs = attrs + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.next.function_params + + def require_space_after_declSpecs(self) -> bool: + return self.const or self.volatile or self.restrict or \ + len(self.attrs) > 0 or \ + self.next.require_space_after_declSpecs() + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['*'] + res.append(transform(self.attrs)) + if len(self.attrs) != 0 and (self.restrict or self.volatile or self.const): + res.append(' ') + if self.restrict: + res.append('restrict') + if self.volatile: + if self.restrict: + res.append(' ') + res.append('volatile') + if self.const: + if self.restrict or self.volatile: + res.append(' ') + res.append('const') + if self.const or self.volatile or self.restrict or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + res.append(' ') + res.append(transform(self.next)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('*', '*') + self.attrs.describe_signature(signode) + if len(self.attrs) != 0 and (self.restrict or self.volatile or self.const): + signode += addnodes.desc_sig_space() + + def _add_anno(signode: TextElement, text: str) -> None: + signode += addnodes.desc_sig_keyword(text, text) + + if self.restrict: + _add_anno(signode, 'restrict') + if self.volatile: + if self.restrict: + signode += addnodes.desc_sig_space() + _add_anno(signode, 'volatile') + if self.const: + if self.restrict or self.volatile: + signode += addnodes.desc_sig_space() + _add_anno(signode, 'const') + if self.const or self.volatile or self.restrict or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + signode += addnodes.desc_sig_space() + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorParen(ASTDeclarator): + def __init__(self, inner: ASTDeclarator, next: ASTDeclarator) -> None: + assert inner + assert next + self.inner = inner + self.next = next + # TODO: we assume the name and params are in inner + + @property + def name(self) -> ASTNestedName: + return self.inner.name + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.inner.function_params + + def require_space_after_declSpecs(self) -> bool: + return True + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.inner)) + res.append(')') + res.append(transform(self.next)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('(', '(') + self.inner.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + self.next.describe_signature(signode, "noneIsName", env, symbol) + + +# Initializer +################################################################################ + +class ASTParenExprList(ASTBaseParenExprList): + def __init__(self, exprs: list[ASTExpression]) -> None: + self.exprs = exprs + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = [transform(e) for e in self.exprs] + return '(%s)' % ', '.join(exprs) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('(', '(') + first = True + for e in self.exprs: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + else: + first = False + e.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTBracedInitList(ASTBase): + def __init__(self, exprs: list[ASTExpression], trailingComma: bool) -> None: + self.exprs = exprs + self.trailingComma = trailingComma + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = ', '.join(transform(e) for e in self.exprs) + trailingComma = ',' if self.trailingComma else '' + return f'{{{exprs}{trailingComma}}}' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('{', '{') + first = True + for e in self.exprs: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + else: + first = False + e.describe_signature(signode, mode, env, symbol) + if self.trailingComma: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_punctuation('}', '}') + + +class ASTInitializer(ASTBase): + def __init__(self, value: ASTBracedInitList | ASTExpression, + hasAssign: bool = True) -> None: + self.value = value + self.hasAssign = hasAssign + + def _stringify(self, transform: StringifyTransform) -> str: + val = transform(self.value) + if self.hasAssign: + return ' = ' + val + else: + return val + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.hasAssign: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + self.value.describe_signature(signode, 'markType', env, symbol) + + +class ASTType(ASTBase): + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: + assert declSpecs + assert decl + self.declSpecs = declSpecs + self.decl = decl + + @property + def name(self) -> ASTNestedName: + return self.decl.name + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.decl.function_params + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + declSpecs = transform(self.declSpecs) + res.append(declSpecs) + if self.decl.require_space_after_declSpecs() and len(declSpecs) > 0: + res.append(' ') + res.append(transform(self.decl)) + return ''.join(res) + + def get_type_declaration_prefix(self) -> str: + if self.declSpecs.trailingTypeSpec: + return 'typedef' + else: + return 'type' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.declSpecs.describe_signature(signode, 'markType', env, symbol) + if (self.decl.require_space_after_declSpecs() and + len(str(self.declSpecs)) > 0): + signode += addnodes.desc_sig_space() + # for parameters that don't really declare new names we get 'markType', + # this should not be propagated, but be 'noneIsName'. + if mode == 'markType': + mode = 'noneIsName' + self.decl.describe_signature(signode, mode, env, symbol) + + +class ASTTypeWithInit(ASTBase): + def __init__(self, type: ASTType, init: ASTInitializer) -> None: + self.type = type + self.init = init + + @property + def name(self) -> ASTNestedName: + return self.type.name + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return self.type.get_id(version, objectType, symbol) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.type)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.type.describe_signature(signode, mode, env, symbol) + if self.init: + self.init.describe_signature(signode, mode, env, symbol) + + +class ASTMacroParameter(ASTBase): + def __init__(self, arg: ASTNestedName | None, ellipsis: bool = False, + variadic: bool = False) -> None: + self.arg = arg + self.ellipsis = ellipsis + self.variadic = variadic + + def _stringify(self, transform: StringifyTransform) -> str: + if self.ellipsis: + return '...' + elif self.variadic: + return transform(self.arg) + '...' + else: + return transform(self.arg) + + def describe_signature(self, signode: Any, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.ellipsis: + signode += addnodes.desc_sig_punctuation('...', '...') + elif self.variadic: + name = str(self) + signode += addnodes.desc_sig_name(name, name) + else: + self.arg.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTMacro(ASTBase): + def __init__(self, ident: ASTNestedName, args: list[ASTMacroParameter] | None) -> None: + self.ident = ident + self.args = args + + @property + def name(self) -> ASTNestedName: + return self.ident + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.ident)) + if self.args is not None: + res.append('(') + first = True + for arg in self.args: + if not first: + res.append(', ') + first = False + res.append(transform(arg)) + res.append(')') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.ident.describe_signature(signode, mode, env, symbol) + if self.args is None: + return + paramlist = addnodes.desc_parameterlist() + for arg in self.args: + param = addnodes.desc_parameter('', '', noemph=True) + arg.describe_signature(param, 'param', env, symbol=symbol) + paramlist += param + signode += paramlist + + +class ASTStruct(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTUnion(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTEnum(ASTBase): + def __init__(self, name: ASTNestedName) -> None: + self.name = name + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTEnumerator(ASTBase): + def __init__(self, name: ASTNestedName, init: ASTInitializer | None, + attrs: ASTAttributeList) -> None: + self.name = name + self.init = init + self.attrs = attrs + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.name)) + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.attrs)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol) + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.attrs.describe_signature(signode) + if self.init: + self.init.describe_signature(signode, 'markType', env, symbol) + + +class ASTDeclaration(ASTBaseBase): + def __init__(self, objectType: str, directiveType: str | None, + declaration: DeclarationType | ASTFunctionParameter, + semicolon: bool = False) -> None: + self.objectType = objectType + self.directiveType = directiveType + self.declaration = declaration + self.semicolon = semicolon + + self.symbol: Symbol = None + # set by CObject._add_enumerator_to_parent + self.enumeratorScopedSymbol: Symbol = None + + def clone(self) -> ASTDeclaration: + return ASTDeclaration(self.objectType, self.directiveType, + self.declaration.clone(), self.semicolon) + + @property + def name(self) -> ASTNestedName: + decl = cast(DeclarationType, self.declaration) + return decl.name + + @property + def function_params(self) -> list[ASTFunctionParameter] | None: + if self.objectType != 'function': + return None + decl = cast(ASTType, self.declaration) + return decl.function_params + + def get_id(self, version: int, prefixed: bool = True) -> str: + if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: + return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed) + id_ = self.declaration.get_id(version, self.objectType, self.symbol) + if prefixed: + return _id_prefix[version] + id_ + else: + return id_ + + def get_newest_id(self) -> str: + return self.get_id(_max_id, True) + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.declaration) + if self.semicolon: + res += ';' + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, options: dict) -> None: + verify_description_mode(mode) + assert self.symbol + # The caller of the domain added a desc_signature node. + # Always enable multiline: + signode['is_multiline'] = True + # Put each line in a desc_signature_line node. + mainDeclNode = addnodes.desc_signature_line() + mainDeclNode.sphinx_line_type = 'declarator' + mainDeclNode['add_permalink'] = not self.symbol.isRedeclaration + signode += mainDeclNode + + if self.objectType in {'member', 'function', 'macro'}: + pass + elif self.objectType == 'struct': + mainDeclNode += addnodes.desc_sig_keyword('struct', 'struct') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'union': + mainDeclNode += addnodes.desc_sig_keyword('union', 'union') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'enum': + mainDeclNode += addnodes.desc_sig_keyword('enum', 'enum') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'enumerator': + mainDeclNode += addnodes.desc_sig_keyword('enumerator', 'enumerator') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'type': + decl = cast(ASTType, self.declaration) + prefix = decl.get_type_declaration_prefix() + mainDeclNode += addnodes.desc_sig_keyword(prefix, prefix) + mainDeclNode += addnodes.desc_sig_space() + else: + raise AssertionError + self.declaration.describe_signature(mainDeclNode, mode, env, self.symbol) + if self.semicolon: + mainDeclNode += addnodes.desc_sig_punctuation(';', ';') + + +class SymbolLookupResult: + def __init__(self, symbols: Iterator[Symbol], parentSymbol: Symbol, + ident: ASTIdentifier) -> None: + self.symbols = symbols + self.parentSymbol = parentSymbol + self.ident = ident + + +class LookupKey: + def __init__(self, data: list[tuple[ASTIdentifier, str]]) -> None: + self.data = data + + def __str__(self) -> str: + inner = ', '.join(f"({ident}, {id_})" for ident, id_ in self.data) + return f'[{inner}]' + + +class Symbol: + debug_indent = 0 + debug_indent_string = " " + debug_lookup = False + debug_show_tree = False + + def __copy__(self): + raise AssertionError # shouldn't happen + + def __deepcopy__(self, memo): + if self.parent: + raise AssertionError # shouldn't happen + # the domain base class makes a copy of the initial data, which is fine + return Symbol(None, None, None, None, None) + + @staticmethod + def debug_print(*args: Any) -> None: + logger.debug(Symbol.debug_indent_string * Symbol.debug_indent, end="") + logger.debug(*args) + + def _assert_invariants(self) -> None: + if not self.parent: + # parent == None means global scope, so declaration means a parent + assert not self.declaration + assert not self.docname + else: + if self.declaration: + assert self.docname + + def __setattr__(self, key: str, value: Any) -> None: + if key == "children": + raise AssertionError + return super().__setattr__(key, value) + + def __init__( + self, + parent: Symbol, + ident: ASTIdentifier, + declaration: ASTDeclaration | None, + docname: str | None, + line: int | None, + ) -> None: + self.parent = parent + # declarations in a single directive are linked together + self.siblingAbove: Symbol = None + self.siblingBelow: Symbol = None + self.ident = ident + self.declaration = declaration + self.docname = docname + self.line = line + self.isRedeclaration = False + self._assert_invariants() + + # Remember to modify Symbol.remove if modifications to the parent change. + self._children: list[Symbol] = [] + self._anonChildren: list[Symbol] = [] + # note: _children includes _anonChildren + if self.parent: + self.parent._children.append(self) + if self.declaration: + self.declaration.symbol = self + + # Do symbol addition after self._children has been initialised. + self._add_function_params() + + def _fill_empty(self, declaration: ASTDeclaration, docname: str, line: int) -> None: + self._assert_invariants() + assert self.declaration is None + assert self.docname is None + assert self.line is None + assert declaration is not None + assert docname is not None + assert line is not None + self.declaration = declaration + self.declaration.symbol = self + self.docname = docname + self.line = line + self._assert_invariants() + # and symbol addition should be done as well + self._add_function_params() + + def _add_function_params(self) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_function_params:") + # Note: we may be called from _fill_empty, so the symbols we want + # to add may actually already be present (as empty symbols). + + # add symbols for function parameters, if any + if self.declaration is not None and self.declaration.function_params is not None: + for p in self.declaration.function_params: + if p.arg is None: + continue + nn = p.arg.name + if nn is None: + continue + # (comparing to the template params: we have checked that we are a declaration) + decl = ASTDeclaration('functionParam', None, p) + assert not nn.rooted + assert len(nn.names) == 1 + self._add_symbols(nn, decl, self.docname, self.line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + + def remove(self) -> None: + if self.parent is None: + return + assert self in self.parent._children + self.parent._children.remove(self) + self.parent = None + + def clear_doc(self, docname: str) -> None: + for sChild in self._children: + sChild.clear_doc(docname) + if sChild.declaration and sChild.docname == docname: + sChild.declaration = None + sChild.docname = None + sChild.line = None + if sChild.siblingAbove is not None: + sChild.siblingAbove.siblingBelow = sChild.siblingBelow + if sChild.siblingBelow is not None: + sChild.siblingBelow.siblingAbove = sChild.siblingAbove + sChild.siblingAbove = None + sChild.siblingBelow = None + + def get_all_symbols(self) -> Iterator[Symbol]: + yield self + for sChild in self._children: + yield from sChild.get_all_symbols() + + @property + def children(self) -> Iterator[Symbol]: + yield from self._children + + @property + def children_recurse_anon(self) -> Iterator[Symbol]: + for c in self._children: + yield c + if not c.ident.is_anon(): + continue + yield from c.children_recurse_anon + + def get_lookup_key(self) -> LookupKey: + # The pickle files for the environment and for each document are distinct. + # The environment has all the symbols, but the documents has xrefs that + # must know their scope. A lookup key is essentially a specification of + # how to find a specific symbol. + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + key = [] + for s in symbols: + if s.declaration is not None: + # TODO: do we need the ID? + key.append((s.ident, s.declaration.get_newest_id())) + else: + key.append((s.ident, None)) + return LookupKey(key) + + def get_full_nested_name(self) -> ASTNestedName: + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + names = [] + for s in symbols: + names.append(s.ident) + return ASTNestedName(names, rooted=False) + + def _find_first_named_symbol(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool) -> Symbol | None: + # TODO: further simplification from C++ to C + if Symbol.debug_lookup: + Symbol.debug_print("_find_first_named_symbol ->") + res = self._find_named_symbols(ident, matchSelf, recurseInAnon, + searchInSiblings=False) + try: + return next(res) + except StopIteration: + return None + + def _find_named_symbols(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool, + searchInSiblings: bool) -> Iterator[Symbol]: + # TODO: further simplification from C++ to C + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_find_named_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("ident: ", ident) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + def candidates() -> Generator[Symbol, None, None]: + s = self + if Symbol.debug_lookup: + Symbol.debug_print("searching in self:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + while True: + if matchSelf: + yield s + if recurseInAnon: + yield from s.children_recurse_anon + else: + yield from s._children + + if s.siblingAbove is None: + break + s = s.siblingAbove + if Symbol.debug_lookup: + Symbol.debug_print("searching in sibling:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + + for s in candidates(): + if Symbol.debug_lookup: + Symbol.debug_print("candidate:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + if s.ident == ident: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("matches") + Symbol.debug_indent -= 3 + yield s + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + + def _symbol_lookup( + self, + nestedName: ASTNestedName, + onMissingQualifiedSymbol: Callable[[Symbol, ASTIdentifier], Symbol | None], + ancestorLookupType: str | None, + matchSelf: bool, + recurseInAnon: bool, + searchInSiblings: bool, + ) -> SymbolLookupResult | None: + # TODO: further simplification from C++ to C + # ancestorLookupType: if not None, specifies the target type of the lookup + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_symbol_lookup:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("ancestorLookupType:", ancestorLookupType) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + names = nestedName.names + + # find the right starting point for lookup + parentSymbol = self + if nestedName.rooted: + while parentSymbol.parent: + parentSymbol = parentSymbol.parent + if ancestorLookupType is not None: + # walk up until we find the first identifier + firstName = names[0] + while parentSymbol.parent: + if parentSymbol.find_identifier(firstName, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings): + break + parentSymbol = parentSymbol.parent + + if Symbol.debug_lookup: + Symbol.debug_print("starting point:") + logger.debug(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # and now the actual lookup + for ident in names[:-1]: + symbol = parentSymbol._find_first_named_symbol( + ident, matchSelf=matchSelf, recurseInAnon=recurseInAnon) + if symbol is None: + symbol = onMissingQualifiedSymbol(parentSymbol, ident) + if symbol is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + # We have now matched part of a nested name, and need to match more + # so even if we should matchSelf before, we definitely shouldn't + # even more. (see also issue #2666) + matchSelf = False + parentSymbol = symbol + + if Symbol.debug_lookup: + Symbol.debug_print("handle last name from:") + logger.debug(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # handle the last name + ident = names[-1] + + symbols = parentSymbol._find_named_symbols( + ident, matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings) + if Symbol.debug_lookup: + symbols = list(symbols) # type: ignore[assignment] + Symbol.debug_indent -= 2 + return SymbolLookupResult(symbols, parentSymbol, ident) + + def _add_symbols( + self, + nestedName: ASTNestedName, + declaration: ASTDeclaration | None, + docname: str | None, + line: int | None, + ) -> Symbol: + # TODO: further simplification from C++ to C + # Used for adding a whole path of symbols, where the last may or may not + # be an actual declaration. + + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print(f"location: {docname}:{line}") + + def onMissingQualifiedSymbol(parentSymbol: Symbol, ident: ASTIdentifier) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols, onMissingQualifiedSymbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", ident) + Symbol.debug_indent -= 2 + return Symbol(parent=parentSymbol, ident=ident, + declaration=None, docname=None, line=None) + + lookupResult = self._symbol_lookup(nestedName, + onMissingQualifiedSymbol, + ancestorLookupType=None, + matchSelf=False, + recurseInAnon=False, + searchInSiblings=False) + assert lookupResult is not None # we create symbols all the way, so that can't happen + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, no symbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", lookupResult.ident) + Symbol.debug_print("declaration: ", declaration) + Symbol.debug_print(f"location: {docname}:{line}") + Symbol.debug_indent -= 1 + symbol = Symbol(parent=lookupResult.parentSymbol, + ident=lookupResult.ident, + declaration=declaration, + docname=docname, line=line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return symbol + + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("number symbols:", len(symbols)) + Symbol.debug_indent -= 1 + + if not declaration: + if Symbol.debug_lookup: + Symbol.debug_print("no declaration") + Symbol.debug_indent -= 2 + # good, just a scope creation + # TODO: what if we have more than one symbol? + return symbols[0] + + noDecl = [] + withDecl = [] + dupDecl = [] + for s in symbols: + if s.declaration is None: + noDecl.append(s) + elif s.isRedeclaration: + dupDecl.append(s) + else: + withDecl.append(s) + if Symbol.debug_lookup: + Symbol.debug_print("#noDecl: ", len(noDecl)) + Symbol.debug_print("#withDecl:", len(withDecl)) + Symbol.debug_print("#dupDecl: ", len(dupDecl)) + + # With partial builds we may start with a large symbol tree stripped of declarations. + # Essentially any combination of noDecl, withDecl, and dupDecls seems possible. + # TODO: make partial builds fully work. What should happen when the primary symbol gets + # deleted, and other duplicates exist? The full document should probably be rebuild. + + # First check if one of those with a declaration matches. + # If it's a function, we need to compare IDs, + # otherwise there should be only one symbol with a declaration. + def makeCandSymbol() -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_print("begin: creating candidate symbol") + symbol = Symbol(parent=lookupResult.parentSymbol, + ident=lookupResult.ident, + declaration=declaration, + docname=docname, line=line) + if Symbol.debug_lookup: + Symbol.debug_print("end: creating candidate symbol") + return symbol + + if len(withDecl) == 0: + candSymbol = None + else: + candSymbol = makeCandSymbol() + + def handleDuplicateDeclaration(symbol: Symbol, candSymbol: Symbol) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("redeclaration") + Symbol.debug_indent -= 1 + Symbol.debug_indent -= 2 + # Redeclaration of the same symbol. + # Let the new one be there, but raise an error to the client + # so it can use the real symbol as subscope. + # This will probably result in a duplicate id warning. + 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 + # if there is an empty symbol, fill that one + if len(noDecl) == 0: + if Symbol.debug_lookup: + Symbol.debug_print( + "no match, no empty, candSybmol is not None?:", candSymbol is not None, + ) + Symbol.debug_indent -= 2 + if candSymbol is not None: + return candSymbol + else: + return makeCandSymbol() + else: + if Symbol.debug_lookup: + Symbol.debug_print( + "no match, but fill an empty declaration, candSybmol is not None?:", + candSymbol is not None) + Symbol.debug_indent -= 2 + if candSymbol is not None: + candSymbol.remove() + # assert len(noDecl) == 1 + # TODO: enable assertion when we at some point find out how to do cleanup + # for now, just take the first one, it should work fine ... right? + symbol = noDecl[0] + # If someone first opened the scope, and then later + # declares it, e.g, + # .. namespace:: Test + # .. namespace:: nullptr + # .. class:: Test + symbol._fill_empty(declaration, docname, line) + return symbol + + def merge_with(self, other: Symbol, docnames: list[str], + env: BuildEnvironment) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("merge_with:") + assert other is not None + for otherChild in other._children: + ourChild = self._find_first_named_symbol( + ident=otherChild.ident, matchSelf=False, + recurseInAnon=False) + if ourChild is None: + # TODO: hmm, should we prune by docnames? + self._children.append(otherChild) + otherChild.parent = self + otherChild._assert_invariants() + continue + if otherChild.declaration and otherChild.docname in docnames: + if not ourChild.declaration: + ourChild._fill_empty(otherChild.declaration, + otherChild.docname, otherChild.line) + elif ourChild.docname != otherChild.docname: + name = str(ourChild.declaration) + msg = __("Duplicate C declaration, also defined at %s:%s.\n" + "Declaration is '.. c:%s:: %s'.") + msg = msg % (ourChild.docname, ourChild.line, + ourChild.declaration.directiveType, name) + logger.warning(msg, location=(otherChild.docname, otherChild.line)) + else: + # Both have declarations, and in the same docname. + # This can apparently happen, it should be safe to + # just ignore it, right? + pass + ourChild.merge_with(otherChild, docnames, env) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + + def add_name(self, nestedName: ASTNestedName) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_name:") + res = self._add_symbols(nestedName, declaration=None, docname=None, line=None) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def add_declaration(self, declaration: ASTDeclaration, + docname: str, line: int) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_declaration:") + assert declaration is not None + assert docname is not None + assert line is not None + nestedName = declaration.name + res = self._add_symbols(nestedName, declaration, docname, line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def find_identifier(self, ident: ASTIdentifier, + matchSelf: bool, recurseInAnon: bool, searchInSiblings: bool, + ) -> Symbol | None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_identifier:") + Symbol.debug_indent += 1 + Symbol.debug_print("ident: ", ident) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings:", searchInSiblings) + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + current = self + while current is not None: + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + Symbol.debug_print("trying:") + logger.debug(current.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + if matchSelf and current.ident == ident: + return current + children = current.children_recurse_anon if recurseInAnon else current._children + for s in children: + if s.ident == ident: + return s + if not searchInSiblings: + break + current = current.siblingAbove + return None + + def direct_lookup(self, key: LookupKey) -> Symbol | None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("direct_lookup:") + Symbol.debug_indent += 1 + s = self + for name, id_ in key.data: + res = None + for cand in s._children: + if cand.ident == name: + res = cand + break + s = res + if Symbol.debug_lookup: + Symbol.debug_print("name: ", name) + Symbol.debug_print("id: ", id_) + if s is not None: + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + else: + Symbol.debug_print("not found") + if s is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return s + + def find_declaration(self, nestedName: ASTNestedName, typ: str, + matchSelf: bool, recurseInAnon: bool) -> Symbol | None: + # templateShorthand: missing template parameter lists for templates is ok + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_declaration:") + + def onMissingQualifiedSymbol( + parentSymbol: Symbol, + ident: ASTIdentifier, + ) -> Symbol | None: + return None + + lookupResult = self._symbol_lookup(nestedName, + onMissingQualifiedSymbol, + ancestorLookupType=typ, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + if lookupResult is None: + return None + + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + return None + return symbols[0] + + def to_string(self, indent: int) -> str: + res = [Symbol.debug_indent_string * indent] + if not self.parent: + res.append('::') + else: + if self.ident: + res.append(str(self.ident)) + else: + res.append(str(self.declaration)) + if self.declaration: + res.append(": ") + if self.isRedeclaration: + res.append('!!duplicate!! ') + res.append(str(self.declaration)) + if self.docname: + res.append('\t(') + res.append(self.docname) + res.append(')') + res.append('\n') + return ''.join(res) + + def dump(self, indent: int) -> str: + res = [self.to_string(indent)] + for c in self._children: + res.append(c.dump(indent + 1)) + return ''.join(res) + + +class DefinitionParser(BaseParser): + @property + def language(self) -> str: + return 'C' + + @property + def id_attributes(self): + return self.config.c_id_attributes + + @property + def paren_attributes(self): + return self.config.c_paren_attributes + + def _parse_string(self) -> str | None: + if self.current_char != '"': + return None + startPos = self.pos + self.pos += 1 + escape = False + while True: + if self.eof: + self.fail("Unexpected end during inside string.") + elif self.current_char == '"' and not escape: + self.pos += 1 + break + elif self.current_char == '\\': + escape = True + else: + escape = False + self.pos += 1 + return self.definition[startPos:self.pos] + + def _parse_literal(self) -> ASTLiteral | None: + # -> integer-literal + # | character-literal + # | floating-literal + # | string-literal + # | boolean-literal -> "false" | "true" + self.skip_ws() + if self.skip_word('true'): + return ASTBooleanLiteral(True) + if self.skip_word('false'): + return ASTBooleanLiteral(False) + pos = self.pos + if self.match(float_literal_re): + self.match(float_literal_suffix_re) + return ASTNumberLiteral(self.definition[pos:self.pos]) + for regex in [binary_literal_re, hex_literal_re, + integer_literal_re, octal_literal_re]: + if self.match(regex): + self.match(integers_literal_suffix_re) + return ASTNumberLiteral(self.definition[pos:self.pos]) + + string = self._parse_string() + if string is not None: + return ASTStringLiteral(string) + + # character-literal + if self.match(char_literal_re): + prefix = self.last_match.group(1) # may be None when no prefix + data = self.last_match.group(2) + try: + return ASTCharLiteral(prefix, data) + except UnicodeDecodeError as e: + self.fail("Can not handle character literal. Internal error was: %s" % e) + except UnsupportedMultiCharacterCharLiteral: + self.fail("Can not handle character literal" + " resulting in multiple decoded characters.") + return None + + def _parse_paren_expression(self) -> ASTExpression | None: + # "(" expression ")" + if self.current_char != '(': + return None + self.pos += 1 + res = self._parse_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' in end of parenthesized expression.") + return ASTParenExpr(res) + + def _parse_primary_expression(self) -> ASTExpression | None: + # literal + # "(" expression ")" + # id-expression -> we parse this with _parse_nested_name + self.skip_ws() + res: ASTExpression | None = self._parse_literal() + if res is not None: + return res + 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 + + def _parse_initializer_list(self, name: str, open: str, close: str, + ) -> tuple[list[ASTExpression], bool]: + # Parse open and close with the actual initializer-list in between + # -> initializer-clause '...'[opt] + # | initializer-list ',' initializer-clause '...'[opt] + # TODO: designators + self.skip_ws() + if not self.skip_string_and_ws(open): + return None, None + if self.skip_string(close): + return [], False + + exprs = [] + trailingComma = False + while True: + self.skip_ws() + expr = self._parse_expression() + self.skip_ws() + exprs.append(expr) + self.skip_ws() + if self.skip_string(close): + break + if not self.skip_string_and_ws(','): + self.fail(f"Error in {name}, expected ',' or '{close}'.") + if self.current_char == close and close == '}': + self.pos += 1 + trailingComma = True + break + return exprs, trailingComma + + def _parse_paren_expression_list(self) -> ASTParenExprList | None: + # -> '(' expression-list ')' + # though, we relax it to also allow empty parens + # as it's needed in some cases + # + # expression-list + # -> initializer-list + exprs, trailingComma = self._parse_initializer_list("parenthesized expression-list", + '(', ')') + if exprs is None: + return None + return ASTParenExprList(exprs) + + def _parse_braced_init_list(self) -> ASTBracedInitList | None: + # -> '{' initializer-list ','[opt] '}' + # | '{' '}' + exprs, trailingComma = self._parse_initializer_list("braced-init-list", '{', '}') + if exprs is None: + return None + return ASTBracedInitList(exprs, trailingComma) + + def _parse_postfix_expression(self) -> ASTPostfixExpr: + # -> primary + # | postfix "[" expression "]" + # | postfix "[" braced-init-list [opt] "]" + # | postfix "(" expression-list [opt] ")" + # | postfix "." id-expression // taken care of in primary by nested name + # | postfix "->" id-expression + # | postfix "++" + # | postfix "--" + + prefix = self._parse_primary_expression() + + # and now parse postfixes + postFixes: list[ASTPostfixOp] = [] + while True: + self.skip_ws() + if self.skip_string_and_ws('['): + expr = self._parse_expression() + self.skip_ws() + if not self.skip_string(']'): + 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 arrow + self.pos -= 3 + else: + name = self._parse_nested_name() + postFixes.append(ASTPostfixMemberOfPointer(name)) + continue + if self.skip_string('++'): + postFixes.append(ASTPostfixInc()) + continue + if self.skip_string('--'): + postFixes.append(ASTPostfixDec()) + continue + lst = self._parse_paren_expression_list() + if lst is not None: + postFixes.append(ASTPostfixCallExpr(lst)) + continue + break + return ASTPostfixExpr(prefix, postFixes) + + def _parse_unary_expression(self) -> ASTExpression: + # -> postfix + # | "++" cast + # | "--" cast + # | unary-operator cast -> (* | & | + | - | ! | ~) cast + # The rest: + # | "sizeof" unary + # | "sizeof" "(" type-id ")" + # | "alignof" "(" type-id ")" + self.skip_ws() + for op in _expression_unary_ops: + # TODO: hmm, should we be able to backtrack here? + if op[0] in 'cn': + res = self.skip_word(op) + else: + res = self.skip_string(op) + if res: + expr = self._parse_cast_expression() + return ASTUnaryOpExpr(op, expr) + if self.skip_word_and_ws('sizeof'): + if self.skip_string_and_ws('('): + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'sizeof'.") + return ASTSizeofType(typ) + expr = self._parse_unary_expression() + return ASTSizeofExpr(expr) + if self.skip_word_and_ws('alignof'): + if not self.skip_string_and_ws('('): + self.fail("Expecting '(' after 'alignof'.") + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'alignof'.") + return ASTAlignofExpr(typ) + return self._parse_postfix_expression() + + def _parse_cast_expression(self) -> ASTExpression: + # -> unary | "(" type-id ")" cast + pos = self.pos + self.skip_ws() + if self.skip_string('('): + try: + typ = self._parse_type(False) + if not self.skip_string(')'): + self.fail("Expected ')' in cast expression.") + expr = self._parse_cast_expression() + return ASTCastExpr(typ, expr) + except DefinitionError as exCast: + self.pos = pos + try: + return self._parse_unary_expression() + except DefinitionError as exUnary: + errs = [] + errs.append((exCast, "If type cast expression")) + errs.append((exUnary, "If unary expression")) + raise self._make_multi_error(errs, + "Error in cast expression.") from exUnary + else: + return self._parse_unary_expression() + + def _parse_logical_or_expression(self) -> ASTExpression: + # logical-or = logical-and || + # logical-and = inclusive-or && + # inclusive-or = exclusive-or | + # exclusive-or = and ^ + # and = equality & + # equality = relational ==, != + # relational = shift <, >, <=, >= + # shift = additive <<, >> + # additive = multiplicative +, - + # multiplicative = pm *, /, % + # pm = cast .*, ->* + def _parse_bin_op_expr(self, opId): + if opId + 1 == len(_expression_bin_ops): + def parser() -> ASTExpression: + return self._parse_cast_expression() + else: + def parser() -> ASTExpression: + return _parse_bin_op_expr(self, opId + 1) + exprs = [] + ops = [] + exprs.append(parser()) + while True: + self.skip_ws() + pos = self.pos + oneMore = False + for op in _expression_bin_ops[opId]: + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue + if op == '&' and self.current_char == '&': + # don't split the && 'token' + self.pos -= 1 + # and btw. && has lower precedence, so we are done + break + try: + expr = parser() + exprs.append(expr) + ops.append(op) + oneMore = True + break + except DefinitionError: + self.pos = pos + if not oneMore: + break + return ASTBinOpExpr(exprs, ops) + return _parse_bin_op_expr(self, 0) + + def _parse_conditional_expression_tail(self, orExprHead: Any) -> ASTExpression | None: + # -> "?" expression ":" assignment-expression + return None + + def _parse_assignment_expression(self) -> ASTExpression: + # -> conditional-expression + # | logical-or-expression assignment-operator initializer-clause + # -> conditional-expression -> + # logical-or-expression + # | logical-or-expression "?" expression ":" assignment-expression + # | logical-or-expression assignment-operator initializer-clause + exprs = [] + ops = [] + orExpr = self._parse_logical_or_expression() + exprs.append(orExpr) + # TODO: handle ternary with _parse_conditional_expression_tail + while True: + oneMore = False + self.skip_ws() + for op in _expression_assignment_ops: + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue + expr = self._parse_logical_or_expression() + exprs.append(expr) + ops.append(op) + oneMore = True + if not oneMore: + break + return ASTAssignmentExpr(exprs, ops) + + def _parse_constant_expression(self) -> ASTExpression: + # -> conditional-expression + orExpr = self._parse_logical_or_expression() + # TODO: use _parse_conditional_expression_tail + return orExpr + + def _parse_expression(self) -> ASTExpression: + # -> assignment-expression + # | expression "," assignment-expression + # TODO: actually parse the second production + return self._parse_assignment_expression() + + def _parse_expression_fallback( + self, end: list[str], + parser: Callable[[], ASTExpression], + allow: bool = True) -> ASTExpression: + # Stupidly "parse" an expression. + # 'end' should be a list of characters which ends the expression. + + # first try to use the provided parser + prevPos = self.pos + try: + return parser() + except DefinitionError as e: + # some places (e.g., template parameters) we really don't want to use fallback, + # and for testing we may want to globally disable it + if not allow or not self.allowFallbackExpressionParsing: + raise + self.warn("Parsing of expression failed. Using fallback parser." + " Error was:\n%s" % e) + self.pos = prevPos + # and then the fallback scanning + assert end is not None + self.skip_ws() + startPos = self.pos + if self.match(_string_re): + value = self.matched_text + else: + # TODO: add handling of more bracket-like things, and quote handling + brackets = {'(': ')', '{': '}', '[': ']'} + symbols: list[str] = [] + while not self.eof: + if (len(symbols) == 0 and self.current_char in end): + break + if self.current_char in brackets: + symbols.append(brackets[self.current_char]) + elif len(symbols) > 0 and self.current_char == symbols[-1]: + symbols.pop() + self.pos += 1 + if len(end) > 0 and self.eof: + self.fail("Could not find end of expression starting at %d." + % startPos) + value = self.definition[startPos:self.pos].strip() + return ASTFallbackExpr(value.strip()) + + def _parse_nested_name(self) -> ASTNestedName: + names: list[Any] = [] + + self.skip_ws() + rooted = False + if self.skip_string('.'): + rooted = True + while 1: + self.skip_ws() + if not self.match(identifier_re): + self.fail("Expected identifier in nested name.") + identifier = self.matched_text + # make sure there isn't a keyword + if identifier in _keywords: + self.fail("Expected identifier in nested name, " + "got keyword: %s" % identifier) + if self.matched_text in self.config.c_extra_keywords: + msg = "Expected identifier, got user-defined keyword: %s." \ + + " Remove it from c_extra_keywords to allow it as identifier.\n" \ + + "Currently c_extra_keywords is %s." + self.fail(msg % (self.matched_text, + str(self.config.c_extra_keywords))) + ident = ASTIdentifier(identifier) + names.append(ident) + + self.skip_ws() + if not self.skip_string('.'): + break + return ASTNestedName(names, rooted) + + def _parse_simple_type_specifier(self) -> str | None: + if self.match(_simple_type_specifiers_re): + return self.matched_text + for t in ('bool', 'complex', 'imaginary'): + if t in self.config.c_extra_keywords: + if self.skip_word(t): + return t + return None + + def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental | None: + names: list[str] = [] + + self.skip_ws() + while True: + t = self._parse_simple_type_specifier() + if t is None: + break + names.append(t) + self.skip_ws() + if len(names) == 0: + return None + return ASTTrailingTypeSpecFundamental(names) + + def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: + # fundamental types, https://en.cppreference.com/w/c/language/type + # and extensions + self.skip_ws() + res = self._parse_simple_type_specifiers() + if res is not None: + return res + + # prefixed + prefix = None + self.skip_ws() + for k in ('struct', 'enum', 'union'): + if self.skip_word_and_ws(k): + prefix = k + break + + nestedName = self._parse_nested_name() + return ASTTrailingTypeSpecName(prefix, nestedName) + + def _parse_parameters(self, paramMode: str) -> ASTParameters | None: + self.skip_ws() + if not self.skip_string('('): + if paramMode == 'function': + self.fail('Expecting "(" in parameters.') + else: + return None + + args = [] + self.skip_ws() + if not self.skip_string(')'): + while 1: + self.skip_ws() + if self.skip_string('...'): + args.append(ASTFunctionParameter(None, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in parameters.') + break + # note: it seems that function arguments can always be named, + # even in function pointers and similar. + arg = self._parse_type_with_init(outer=None, named='single') + # TODO: parse default parameters # TODO: didn't we just do that? + args.append(ASTFunctionParameter(arg)) + + self.skip_ws() + if self.skip_string(','): + continue + if self.skip_string(')'): + break + self.fail(f'Expecting "," or ")" in parameters, got "{self.current_char}".') + + attrs = self._parse_attribute_list() + return ASTParameters(args, attrs) + + def _parse_decl_specs_simple( + self, outer: str | None, typed: bool, + ) -> ASTDeclSpecsSimple: + """Just parse the simple ones.""" + storage = None + threadLocal = None + inline = None + restrict = None + volatile = None + const = None + attrs = [] + while 1: # accept any permutation of a subset of some decl-specs + self.skip_ws() + if not storage: + if outer == 'member': + if self.skip_word('auto'): + storage = 'auto' + continue + if self.skip_word('register'): + storage = 'register' + continue + if outer in ('member', 'function'): + if self.skip_word('static'): + storage = 'static' + continue + if self.skip_word('extern'): + storage = 'extern' + continue + if outer == 'member' and not threadLocal: + if self.skip_word('thread_local'): + threadLocal = 'thread_local' + continue + if self.skip_word('_Thread_local'): + threadLocal = '_Thread_local' + continue + if outer == 'function' and not inline: + inline = self.skip_word('inline') + if inline: + continue + + if not restrict and typed: + restrict = self.skip_word('restrict') + if restrict: + continue + if not volatile and typed: + volatile = self.skip_word('volatile') + if volatile: + continue + if not const and typed: + const = self.skip_word('const') + if const: + continue + attr = self._parse_attribute() + if attr: + attrs.append(attr) + continue + break + return ASTDeclSpecsSimple(storage, threadLocal, inline, + restrict, volatile, const, ASTAttributeList(attrs)) + + def _parse_decl_specs(self, outer: str | None, typed: bool = True) -> ASTDeclSpecs: + if outer: + if outer not in ('type', 'member', 'function'): + raise Exception('Internal error, unknown outer "%s".' % outer) + leftSpecs = self._parse_decl_specs_simple(outer, typed) + rightSpecs = None + + if typed: + trailing = self._parse_trailing_type_spec() + rightSpecs = self._parse_decl_specs_simple(outer, typed) + else: + trailing = None + return ASTDeclSpecs(outer, leftSpecs, rightSpecs, trailing) + + def _parse_declarator_name_suffix( + self, named: bool | str, paramMode: str, typed: bool, + ) -> ASTDeclarator: + assert named in (True, False, 'single') + # now we should parse the name, and then suffixes + if named == 'single': + if self.match(identifier_re): + if self.matched_text in _keywords: + self.fail("Expected identifier, " + "got keyword: %s" % self.matched_text) + if self.matched_text in self.config.c_extra_keywords: + msg = "Expected identifier, got user-defined keyword: %s." \ + + " Remove it from c_extra_keywords to allow it as identifier.\n" \ + + "Currently c_extra_keywords is %s." + self.fail(msg % (self.matched_text, + str(self.config.c_extra_keywords))) + identifier = ASTIdentifier(self.matched_text) + declId = ASTNestedName([identifier], rooted=False) + else: + declId = None + elif named: + declId = self._parse_nested_name() + else: + declId = None + arrayOps = [] + while 1: + self.skip_ws() + if typed and self.skip_string('['): + self.skip_ws() + static = False + const = False + volatile = False + restrict = False + while True: + if not static: + if self.skip_word_and_ws('static'): + static = True + continue + if not const: + if self.skip_word_and_ws('const'): + const = True + continue + if not volatile: + if self.skip_word_and_ws('volatile'): + volatile = True + continue + if not restrict: + if self.skip_word_and_ws('restrict'): + restrict = True + continue + break + vla = False if static else self.skip_string_and_ws('*') + if vla: + if not self.skip_string(']'): + self.fail("Expected ']' in end of array operator.") + size = None + else: + if self.skip_string(']'): + size = None + else: + + def parser(): + return self._parse_expression() + size = self._parse_expression_fallback([']'], parser) + self.skip_ws() + if not self.skip_string(']'): + self.fail("Expected ']' in end of array operator.") + arrayOps.append(ASTArray(static, const, volatile, restrict, vla, size)) + else: + break + param = self._parse_parameters(paramMode) + if param is None and len(arrayOps) == 0: + # perhaps a bit-field + if named and paramMode == 'type' and typed: + self.skip_ws() + if self.skip_string(':'): + size = self._parse_constant_expression() + return ASTDeclaratorNameBitField(declId=declId, size=size) + return ASTDeclaratorNameParam(declId=declId, arrayOps=arrayOps, + param=param) + + def _parse_declarator(self, named: bool | str, paramMode: str, + typed: bool = True) -> ASTDeclarator: + # 'typed' here means 'parse return type stuff' + if paramMode not in ('type', 'function'): + raise Exception( + "Internal error, unknown paramMode '%s'." % paramMode) + prevErrors = [] + self.skip_ws() + if typed and self.skip_string('*'): + self.skip_ws() + restrict = False + volatile = False + const = False + attrs = [] + while 1: + if not restrict: + restrict = self.skip_word_and_ws('restrict') + if restrict: + continue + if not volatile: + volatile = self.skip_word_and_ws('volatile') + if volatile: + continue + if not const: + const = self.skip_word_and_ws('const') + if const: + continue + attr = self._parse_attribute() + if attr is not None: + attrs.append(attr) + continue + break + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorPtr(next=next, + restrict=restrict, volatile=volatile, const=const, + attrs=ASTAttributeList(attrs)) + if typed and self.current_char == '(': # note: peeking, not skipping + # maybe this is the beginning of params, try that first, + # otherwise assume it's noptr->declarator > ( ptr-declarator ) + pos = self.pos + try: + # assume this is params + res = self._parse_declarator_name_suffix(named, paramMode, + typed) + return res + except DefinitionError as exParamQual: + msg = "If declarator-id with parameters" + if paramMode == 'function': + msg += " (e.g., 'void f(int arg)')" + prevErrors.append((exParamQual, msg)) + self.pos = pos + try: + assert self.current_char == '(' + self.skip_string('(') + # TODO: hmm, if there is a name, it must be in inner, right? + # TODO: hmm, if there must be parameters, they must b + # inside, right? + inner = self._parse_declarator(named, paramMode, typed) + if not self.skip_string(')'): + self.fail("Expected ')' in \"( ptr-declarator )\"") + next = self._parse_declarator(named=False, + paramMode="type", + typed=typed) + return ASTDeclaratorParen(inner=inner, next=next) + except DefinitionError as exNoPtrParen: + self.pos = pos + msg = "If parenthesis in noptr-declarator" + if paramMode == 'function': + msg += " (e.g., 'void (*f(int arg))(double)')" + prevErrors.append((exNoPtrParen, msg)) + header = "Error in declarator" + raise self._make_multi_error(prevErrors, header) from exNoPtrParen + pos = self.pos + try: + return self._parse_declarator_name_suffix(named, paramMode, typed) + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If declarator-id")) + header = "Error in declarator or parameters" + raise self._make_multi_error(prevErrors, header) from e + + def _parse_initializer(self, outer: str | None = None, allowFallback: bool = True, + ) -> ASTInitializer | None: + self.skip_ws() + if outer == 'member' and False: # NoQA: SIM223 # TODO + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit, hasAssign=False) + + if not self.skip_string('='): + return None + + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit) + + if outer == 'member': + fallbackEnd: list[str] = [] + elif outer is None: # function parameter + fallbackEnd = [',', ')'] + else: + self.fail("Internal error, initializer for outer '%s' not " + "implemented." % outer) + + def parser(): + return self._parse_assignment_expression() + + value = self._parse_expression_fallback(fallbackEnd, parser, allow=allowFallback) + return ASTInitializer(value) + + def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: + """ + 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'): + raise Exception('Internal error, unknown outer "%s".' % outer) + assert named + + if outer == 'type': + # We allow type objects to just be a name. + prevErrors = [] + startPos = self.pos + # first try without the type + try: + declSpecs = self._parse_decl_specs(outer=outer, typed=False) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=False) + self.assert_end(allowSemicolon=True) + except DefinitionError as exUntyped: + desc = "If just a name" + prevErrors.append((exUntyped, desc)) + self.pos = startPos + try: + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=True, paramMode=outer) + except DefinitionError as exTyped: + self.pos = startPos + desc = "If typedef-like declaration" + prevErrors.append((exTyped, desc)) + # Retain the else branch for easier debugging. + # TODO: it would be nice to save the previous stacktrace + # and output it here. + if True: + header = "Type must be either just a name or a " + header += "typedef-like declaration." + raise self._make_multi_error(prevErrors, header) from exTyped + else: # NoQA: RET506 + # For testing purposes. + # do it again to get the proper traceback (how do you + # reliably save a traceback when an exception is + # constructed?) + self.pos = startPos + typed = True + declSpecs = self._parse_decl_specs(outer=outer, typed=typed) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=typed) + elif outer == 'function': + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=True, paramMode=outer) + else: + paramMode = 'type' + if outer == 'member': # i.e., member + named = True + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=named, paramMode=paramMode) + return ASTType(declSpecs, decl) + + def _parse_type_with_init(self, named: bool | str, outer: str | None) -> ASTTypeWithInit: + if outer: + assert outer in ('type', 'member', 'function') + type = self._parse_type(outer=outer, named=named) + init = self._parse_initializer(outer=outer) + return ASTTypeWithInit(type, init) + + def _parse_macro(self) -> ASTMacro: + self.skip_ws() + ident = self._parse_nested_name() + if ident is None: + self.fail("Expected identifier in macro definition.") + self.skip_ws() + if not self.skip_string_and_ws('('): + return ASTMacro(ident, None) + if self.skip_string(')'): + return ASTMacro(ident, []) + args = [] + while 1: + self.skip_ws() + if self.skip_string('...'): + args.append(ASTMacroParameter(None, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in macro parameters.') + break + if not self.match(identifier_re): + self.fail("Expected identifier in macro parameters.") + nn = ASTNestedName([ASTIdentifier(self.matched_text)], rooted=False) + # Allow named variadic args: + # https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html + self.skip_ws() + if self.skip_string_and_ws('...'): + args.append(ASTMacroParameter(nn, False, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in macro parameters.') + break + args.append(ASTMacroParameter(nn)) + if self.skip_string_and_ws(','): + continue + if self.skip_string_and_ws(')'): + break + self.fail("Expected identifier, ')', or ',' in macro parameter list.") + return ASTMacro(ident, args) + + def _parse_struct(self) -> ASTStruct: + name = self._parse_nested_name() + return ASTStruct(name) + + def _parse_union(self) -> ASTUnion: + name = self._parse_nested_name() + return ASTUnion(name) + + def _parse_enum(self) -> ASTEnum: + name = self._parse_nested_name() + return ASTEnum(name) + + def _parse_enumerator(self) -> ASTEnumerator: + name = self._parse_nested_name() + attrs = self._parse_attribute_list() + self.skip_ws() + init = None + if self.skip_string('='): + self.skip_ws() + + def parser() -> ASTExpression: + return self._parse_constant_expression() + + initVal = self._parse_expression_fallback([], parser) + init = ASTInitializer(initVal) + return ASTEnumerator(name, init, attrs) + + def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: + if objectType not in ('function', 'member', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + raise Exception('Internal error, unknown objectType "%s".' % objectType) + if directiveType not in ('function', 'member', 'var', + 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): + raise Exception('Internal error, unknown directiveType "%s".' % directiveType) + + declaration: DeclarationType = None + if objectType == 'member': + declaration = self._parse_type_with_init(named=True, outer='member') + elif objectType == 'function': + declaration = self._parse_type(named=True, outer='function') + elif objectType == 'macro': + declaration = self._parse_macro() + elif objectType == 'struct': + declaration = self._parse_struct() + elif objectType == 'union': + declaration = self._parse_union() + elif objectType == 'enum': + declaration = self._parse_enum() + elif objectType == 'enumerator': + declaration = self._parse_enumerator() + elif objectType == 'type': + declaration = self._parse_type(named=True, outer='type') + else: + raise AssertionError + 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() + + def parse_xref_object(self) -> ASTNestedName: + name = self._parse_nested_name() + # if there are '()' left, just skip them + self.skip_ws() + self.skip_string('()') + self.assert_end() + return name + + def parse_expression(self) -> ASTExpression | ASTType: + pos = self.pos + res: ASTExpression | ASTType = None + try: + res = self._parse_expression() + self.skip_ws() + self.assert_end() + except DefinitionError as exExpr: + self.pos = pos + try: + res = self._parse_type(False) + self.skip_ws() + self.assert_end() + except DefinitionError as exType: + header = "Error when parsing (type) expression." + errs = [] + errs.append((exExpr, "If expression")) + errs.append((exType, "If type")) + raise self._make_multi_error(errs, header) from exType + return res + + +def _make_phony_error_name() -> ASTNestedName: + return ASTNestedName([ASTIdentifier("PhonyNameDueToError")], rooted=False) + + +class CObject(ObjectDescription[ASTDeclaration]): + """ + Description of a C language object. + """ + + option_spec: OptionSpec = { + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + 'single-line-parameter-list': directives.flag, + } + + def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: + assert ast.objectType == 'enumerator' + # find the parent, if it exists && is an enum + # then add the name to the parent scope + symbol = ast.symbol + assert symbol + assert symbol.ident is not None + parentSymbol = symbol.parent + assert parentSymbol + if parentSymbol.parent is None: + # TODO: we could warn, but it is somewhat equivalent to + # enumeratorss, without the enum + return # no parent + parentDecl = parentSymbol.declaration + if parentDecl is None: + # the parent is not explicitly declared + # TODO: we could warn, but? + return + if parentDecl.objectType != 'enum': + # TODO: maybe issue a warning, enumerators in non-enums is weird, + # but it is somewhat equivalent to enumeratorss, without the enum + return + if parentDecl.directiveType != 'enum': + return + + targetSymbol = parentSymbol.parent + s = targetSymbol.find_identifier(symbol.ident, matchSelf=False, recurseInAnon=True, + searchInSiblings=False) + if s is not None: + # something is already declared with that name + return + declClone = symbol.declaration.clone() + declClone.enumeratorScopedSymbol = symbol + Symbol(parent=targetSymbol, ident=symbol.ident, + declaration=declClone, + docname=self.env.docname, line=self.get_source_info()[1]) + + def add_target_and_index(self, ast: ASTDeclaration, sig: str, + signode: TextElement) -> None: + ids = [] + for i in range(1, _max_id + 1): + try: + id = ast.get_id(version=i) + ids.append(id) + except NoOldIdError: + assert i < _max_id + # let's keep the newest first + ids = list(reversed(ids)) + newestId = ids[0] + assert newestId # shouldn't be None + + name = ast.symbol.get_full_nested_name().get_display_string().lstrip('.') + if newestId not in self.state.document.ids: + # always add the newest id + assert newestId + signode['ids'].append(newestId) + # only add compatibility ids when there are no conflicts + for id in ids[1:]: + if not id: # is None when the element didn't exist in that version + continue + if id not in self.state.document.ids: + signode['ids'].append(id) + + self.state.document.note_explicit_target(signode) + + if 'no-index-entry' not in self.options: + indexText = self.get_index_text(name) + self.indexnode['entries'].append(('single', indexText, newestId, '', None)) + + @property + def object_type(self) -> str: + raise NotImplementedError + + @property + def display_object_type(self) -> str: + return self.object_type + + def get_index_text(self, name: str) -> str: + return _('%s (C %s)') % (name, self.display_object_type) + + def parse_definition(self, parser: DefinitionParser) -> ASTDeclaration: + return parser.parse_declaration(self.object_type, self.objtype) + + def describe_signature(self, signode: TextElement, ast: ASTDeclaration, + options: dict) -> None: + ast.describe_signature(signode, 'lastIsName', self.env, options) + + def run(self) -> list[Node]: + env = self.state.document.settings.env # from ObjectDescription.run + if 'c:parent_symbol' not in env.temp_data: + root = env.domaindata['c']['root_symbol'] + env.temp_data['c:parent_symbol'] = root + env.ref_context['c:parent_key'] = root.get_lookup_key() + + # When multiple declarations are made in the same directive + # they need to know about each other to provide symbol lookup for function parameters. + # We use last_symbol to store the latest added declaration in a directive. + env.temp_data['c:last_symbol'] = None + return super().run() + + def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: + parentSymbol: Symbol = self.env.temp_data['c:parent_symbol'] + + max_len = (self.env.config.c_maximum_signature_line_length + or self.env.config.maximum_signature_line_length + or 0) + signode['multi_line_parameter_list'] = ( + 'single-line-parameter-list' not in self.options + and (len(sig) > max_len > 0) + ) + + parser = DefinitionParser(sig, location=signode, config=self.env.config) + try: + ast = self.parse_definition(parser) + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=signode) + # It is easier to assume some phony name than handling the error in + # the possibly inner declarations. + name = _make_phony_error_name() + symbol = parentSymbol.add_name(name) + self.env.temp_data['c:last_symbol'] = symbol + raise ValueError from e + + try: + symbol = parentSymbol.add_declaration( + ast, docname=self.env.docname, line=self.get_source_info()[1]) + # append the new declaration to the sibling list + assert symbol.siblingAbove is None + assert symbol.siblingBelow is None + symbol.siblingAbove = self.env.temp_data['c:last_symbol'] + if symbol.siblingAbove is not None: + assert symbol.siblingAbove.siblingBelow is None + symbol.siblingAbove.siblingBelow = symbol + self.env.temp_data['c:last_symbol'] = symbol + except _DuplicateSymbolError as e: + # Assume we are actually in the old symbol, + # instead of the newly created duplicate. + self.env.temp_data['c:last_symbol'] = e.symbol + msg = __("Duplicate C declaration, also defined at %s:%s.\n" + "Declaration is '.. c:%s:: %s'.") + msg = msg % (e.symbol.docname, e.symbol.line, self.display_object_type, sig) + logger.warning(msg, location=signode) + + if ast.objectType == 'enumerator': + self._add_enumerator_to_parent(ast) + + # note: handle_signature may be called multiple time per directive, + # if it has multiple signatures, so don't mess with the original options. + options = dict(self.options) + self.describe_signature(signode, ast, options) + return ast + + def before_content(self) -> None: + lastSymbol: Symbol = self.env.temp_data['c:last_symbol'] + assert lastSymbol + self.oldParentSymbol = self.env.temp_data['c:parent_symbol'] + self.oldParentKey: LookupKey = self.env.ref_context['c:parent_key'] + self.env.temp_data['c:parent_symbol'] = lastSymbol + self.env.ref_context['c:parent_key'] = lastSymbol.get_lookup_key() + + def after_content(self) -> None: + self.env.temp_data['c:parent_symbol'] = self.oldParentSymbol + self.env.ref_context['c:parent_key'] = self.oldParentKey + + +class CMemberObject(CObject): + object_type = 'member' + + @property + def display_object_type(self) -> str: + # the distinction between var and member is only cosmetic + assert self.objtype in ('member', 'var') + return self.objtype + + +_function_doc_field_types = [ + TypedField('parameter', label=_('Parameters'), + names=('param', 'parameter', 'arg', 'argument'), + typerolename='expr', typenames=('type',)), + GroupedField('retval', label=_('Return values'), + names=('retvals', 'retval'), + can_collapse=True), + Field('returnvalue', label=_('Returns'), has_arg=False, + names=('returns', 'return')), + Field('returntype', label=_('Return type'), has_arg=False, + names=('rtype',)), +] + + +class CFunctionObject(CObject): + object_type = 'function' + + doc_field_types = _function_doc_field_types.copy() + + +class CMacroObject(CObject): + object_type = 'macro' + + doc_field_types = _function_doc_field_types.copy() + + +class CStructObject(CObject): + object_type = 'struct' + + +class CUnionObject(CObject): + object_type = 'union' + + +class CEnumObject(CObject): + object_type = 'enum' + + +class CEnumeratorObject(CObject): + object_type = 'enumerator' + + +class CTypeObject(CObject): + object_type = 'type' + + +class CNamespaceObject(SphinxDirective): + """ + This directive is just to tell Sphinx that we're documenting stuff in + namespace foo. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + rootSymbol = self.env.domaindata['c']['root_symbol'] + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + symbol = rootSymbol + stack: list[Symbol] = [] + else: + parser = DefinitionParser(self.arguments[0], + location=self.get_location(), + config=self.env.config) + try: + name = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_location()) + name = _make_phony_error_name() + symbol = rootSymbol.add_name(name) + stack = [symbol] + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() + return [] + + +class CNamespacePushObject(SphinxDirective): + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + return [] + parser = DefinitionParser(self.arguments[0], + location=self.get_location(), + config=self.env.config) + try: + name = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_location()) + name = _make_phony_error_name() + oldParent = self.env.temp_data.get('c:parent_symbol', None) + if not oldParent: + oldParent = self.env.domaindata['c']['root_symbol'] + symbol = oldParent.add_name(name) + stack = self.env.temp_data.get('c:namespace_stack', []) + stack.append(symbol) + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() + return [] + + +class CNamespacePopObject(SphinxDirective): + has_content = False + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + stack = self.env.temp_data.get('c:namespace_stack', None) + if not stack or len(stack) == 0: + logger.warning("C namespace pop on empty stack. Defaulting to global scope.", + location=self.get_location()) + stack = [] + else: + stack.pop() + if len(stack) > 0: + symbol = stack[-1] + else: + symbol = self.env.domaindata['c']['root_symbol'] + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['cp:parent_key'] = symbol.get_lookup_key() + return [] + + +class AliasNode(nodes.Element): + def __init__( + self, + sig: str, + aliasOptions: dict, + document: Any, + env: BuildEnvironment | None = None, + parentKey: LookupKey | None = None, + ) -> None: + super().__init__() + self.sig = sig + self.aliasOptions = aliasOptions + self.document = document + if env is not None: + if 'c:parent_symbol' not in env.temp_data: + root = env.domaindata['c']['root_symbol'] + env.temp_data['c:parent_symbol'] = root + env.ref_context['c:parent_key'] = root.get_lookup_key() + self.parentKey = env.ref_context['c:parent_key'] + else: + assert parentKey is not None + self.parentKey = parentKey + + def copy(self) -> AliasNode: + return self.__class__(self.sig, self.aliasOptions, self.document, + env=None, parentKey=self.parentKey) + + +class AliasTransform(SphinxTransform): + default_priority = ReferencesResolver.default_priority - 1 + + def _render_symbol(self, s: Symbol, maxdepth: int, skipThis: bool, + aliasOptions: dict, renderOptions: dict, + document: Any) -> list[Node]: + if maxdepth == 0: + recurse = True + elif maxdepth == 1: + recurse = False + else: + maxdepth -= 1 + recurse = True + + nodes: list[Node] = [] + if not skipThis: + signode = addnodes.desc_signature('', '') + nodes.append(signode) + s.declaration.describe_signature(signode, 'markName', self.env, renderOptions) + + if recurse: + if skipThis: + childContainer: list[Node] | addnodes.desc = nodes + else: + content = addnodes.desc_content() + desc = addnodes.desc() + content.append(desc) + desc.document = document + desc['domain'] = 'c' + # 'desctype' is a backwards compatible attribute + desc['objtype'] = desc['desctype'] = 'alias' + desc['no-index'] = True + childContainer = desc + + for sChild in s.children: + if sChild.declaration is None: + continue + childNodes = self._render_symbol( + sChild, maxdepth=maxdepth, skipThis=False, + aliasOptions=aliasOptions, renderOptions=renderOptions, + document=document) + childContainer.extend(childNodes) + + if not skipThis and len(desc.children) != 0: + nodes.append(content) + return nodes + + def apply(self, **kwargs: Any) -> None: + for node in self.document.findall(AliasNode): + sig = node.sig + parentKey = node.parentKey + try: + parser = DefinitionParser(sig, location=node, + config=self.env.config) + name = parser.parse_xref_object() + except DefinitionError as e: + logger.warning(e, location=node) + name = None + + if name is None: + # could not be parsed, so stop here + signode = addnodes.desc_signature(sig, '') + signode.clear() + signode += addnodes.desc_name(sig, sig) + node.replace_self(signode) + continue + + rootSymbol: Symbol = self.env.domains['c'].data['root_symbol'] + parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey) + if not parentSymbol: + logger.debug("Target: %s", sig) + logger.debug("ParentKey: %s", parentKey) + logger.debug(rootSymbol.dump(1)) + assert parentSymbol # should be there + + s = parentSymbol.find_declaration( + name, 'any', + matchSelf=True, recurseInAnon=True) + if s is None: + signode = addnodes.desc_signature(sig, '') + node.append(signode) + signode.clear() + signode += addnodes.desc_name(sig, sig) + + logger.warning("Could not find C declaration for alias '%s'." % name, + location=node) + node.replace_self(signode) + continue + # Declarations like .. var:: int Missing::var + # may introduce symbols without declarations. + # But if we skip the root then it is ok to start recursion from it. + if not node.aliasOptions['noroot'] and s.declaration is None: + signode = addnodes.desc_signature(sig, '') + node.append(signode) + signode.clear() + signode += addnodes.desc_name(sig, sig) + + logger.warning( + "Can not render C declaration for alias '%s'. No such declaration." % name, + location=node) + node.replace_self(signode) + continue + + nodes = self._render_symbol(s, maxdepth=node.aliasOptions['maxdepth'], + skipThis=node.aliasOptions['noroot'], + aliasOptions=node.aliasOptions, + renderOptions={}, document=node.document) + node.replace_self(nodes) + + +class CAliasObject(ObjectDescription): + option_spec: OptionSpec = { + 'maxdepth': directives.nonnegative_int, + 'noroot': directives.flag, + } + + def run(self) -> list[Node]: + """ + On purpose this doesn't call the ObjectDescription version, but is based on it. + Each alias signature may expand into multiple real signatures if 'noroot'. + The code is therefore based on the ObjectDescription version. + """ + if ':' in self.name: + self.domain, self.objtype = self.name.split(':', 1) + else: + self.domain, self.objtype = '', self.name + + node = addnodes.desc() + node.document = self.state.document + node['domain'] = self.domain + # 'desctype' is a backwards compatible attribute + node['objtype'] = node['desctype'] = self.objtype + node['no-index'] = True + + self.names: list[str] = [] + aliasOptions = { + 'maxdepth': self.options.get('maxdepth', 1), + 'noroot': 'noroot' in self.options, + } + if aliasOptions['noroot'] and aliasOptions['maxdepth'] == 1: + logger.warning("Error in C alias declaration." + " Requested 'noroot' but 'maxdepth' 1." + " When skipping the root declaration," + " need 'maxdepth' 0 for infinite or at least 2.", + location=self.get_location()) + for sig in self.get_signatures(): + node.append(AliasNode(sig, aliasOptions, self.state.document, env=self.env)) + return [node] + + +class CXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, + has_explicit_title: bool, title: str, target: str) -> tuple[str, str]: + refnode.attributes.update(env.ref_context) + + if not has_explicit_title: + # major hax: replace anon names via simple string manipulation. + # Can this actually fail? + title = anon_identifier_re.sub("[anonymous]", str(title)) + + if not has_explicit_title: + target = target.lstrip('~') # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[0:1] == '~': + title = title[1:] + dot = title.rfind('.') + if dot != -1: + title = title[dot + 1:] + return title, target + + +class CExprRole(SphinxRole): + def __init__(self, asCode: bool) -> None: + super().__init__() + if asCode: + # render the expression as inline code + self.class_type = 'c-expr' + else: + # render the expression as inline text + self.class_type = 'c-texpr' + + def run(self) -> tuple[list[Node], list[system_message]]: + text = self.text.replace('\n', ' ') + parser = DefinitionParser(text, location=self.get_location(), + config=self.env.config) + # attempt to mimic XRefRole classes, except that... + try: + ast = parser.parse_expression() + except DefinitionError as ex: + logger.warning('Unparseable C expression: %r\n%s', text, ex, + location=self.get_location()) + # see below + return [addnodes.desc_inline('c', text, text, classes=[self.class_type])], [] + parentSymbol = self.env.temp_data.get('c:parent_symbol', None) + if parentSymbol is None: + parentSymbol = self.env.domaindata['c']['root_symbol'] + # ...most if not all of these classes should really apply to the individual references, + # not the container node + signode = addnodes.desc_inline('c', classes=[self.class_type]) + ast.describe_signature(signode, 'markType', self.env, parentSymbol) + return [signode], [] + + +class CDomain(Domain): + """C language domain.""" + name = 'c' + label = 'C' + object_types = { + # 'identifier' is the one used for xrefs generated in signatures, not in roles + 'member': ObjType(_('member'), 'var', 'member', 'data', 'identifier'), + 'var': ObjType(_('variable'), 'var', 'member', 'data', 'identifier'), + 'function': ObjType(_('function'), 'func', 'identifier', 'type'), + 'macro': ObjType(_('macro'), 'macro', 'identifier'), + 'struct': ObjType(_('struct'), 'struct', 'identifier', 'type'), + 'union': ObjType(_('union'), 'union', 'identifier', 'type'), + 'enum': ObjType(_('enum'), 'enum', 'identifier', 'type'), + 'enumerator': ObjType(_('enumerator'), 'enumerator', 'identifier'), + 'type': ObjType(_('type'), 'identifier', 'type'), + # generated object types + 'functionParam': ObjType(_('function parameter'), 'identifier', 'var', 'member', 'data'), # noqa: E501 + } + + directives = { + 'member': CMemberObject, + 'var': CMemberObject, + 'function': CFunctionObject, + 'macro': CMacroObject, + 'struct': CStructObject, + 'union': CUnionObject, + 'enum': CEnumObject, + 'enumerator': CEnumeratorObject, + 'type': CTypeObject, + # scope control + 'namespace': CNamespaceObject, + 'namespace-push': CNamespacePushObject, + 'namespace-pop': CNamespacePopObject, + # other + 'alias': CAliasObject, + } + roles = { + 'member': CXRefRole(), + 'data': CXRefRole(), + 'var': CXRefRole(), + 'func': CXRefRole(fix_parens=True), + 'macro': CXRefRole(), + 'struct': CXRefRole(), + 'union': CXRefRole(), + 'enum': CXRefRole(), + 'enumerator': CXRefRole(), + 'type': CXRefRole(), + 'expr': CExprRole(asCode=True), + 'texpr': CExprRole(asCode=False), + } + initial_data: dict[str, Symbol | dict[str, tuple[str, str, str]]] = { + 'root_symbol': Symbol(None, None, None, None, None), + 'objects': {}, # fullname -> docname, node_id, objtype + } + + def clear_doc(self, docname: str) -> None: + if Symbol.debug_show_tree: + logger.debug("clear_doc: %s", docname) + logger.debug("\tbefore:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tbefore end") + + rootSymbol = self.data['root_symbol'] + rootSymbol.clear_doc(docname) + + if Symbol.debug_show_tree: + logger.debug("\tafter:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tafter end") + logger.debug("clear_doc end: %s", docname) + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + if Symbol.debug_show_tree: + logger.debug("process_doc: %s", docname) + logger.debug(self.data['root_symbol'].dump(0)) + logger.debug("process_doc end: %s", docname) + + def process_field_xref(self, pnode: pending_xref) -> None: + pnode.attributes.update(self.env.ref_context) + + def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None: + if Symbol.debug_show_tree: + logger.debug("merge_domaindata:") + logger.debug("\tself:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tself end") + logger.debug("\tother:") + logger.debug(otherdata['root_symbol'].dump(1)) + logger.debug("\tother end") + logger.debug("merge_domaindata end") + + self.data['root_symbol'].merge_with(otherdata['root_symbol'], + docnames, self.env) + ourObjects = self.data['objects'] + for fullname, (fn, id_, objtype) in otherdata['objects'].items(): + if fn in docnames: + if fullname not in ourObjects: + ourObjects[fullname] = (fn, id_, objtype) + # no need to warn on duplicates, the symbol merge already does that + + def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, + contnode: Element) -> tuple[Element | None, str | None]: + parser = DefinitionParser(target, location=node, config=env.config) + try: + name = parser.parse_xref_object() + except DefinitionError as e: + logger.warning('Unparseable C cross-reference: %r\n%s', target, e, + location=node) + return None, None + parentKey: LookupKey = node.get("c:parent_key", None) + rootSymbol = self.data['root_symbol'] + if parentKey: + parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey) + if not parentSymbol: + logger.debug("Target: %s", target) + logger.debug("ParentKey: %s", parentKey) + logger.debug(rootSymbol.dump(1)) + 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 + + # TODO: check role type vs. object type + + declaration = s.declaration + displayName = name.get_display_string() + docname = s.docname + assert docname + + return make_refnode(builder, fromdocname, docname, + declaration.get_newest_id(), contnode, displayName, + ), declaration.objectType + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, + contnode: Element) -> Element | None: + return self._resolve_xref_inner(env, fromdocname, builder, typ, + target, node, contnode)[0] + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + with logging.suppress_logging(): + retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, + 'any', target, node, contnode) + if retnode: + return [('c:' + self.role_for_objtype(objtype), retnode)] + return [] + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + rootSymbol = self.data['root_symbol'] + for symbol in rootSymbol.get_all_symbols(): + if symbol.declaration is None: + continue + assert symbol.docname + fullNestedName = symbol.get_full_nested_name() + name = str(fullNestedName).lstrip('.') + dispname = fullNestedName.get_display_string().lstrip('.') + objectType = symbol.declaration.objectType + docname = symbol.docname + newestId = symbol.declaration.get_newest_id() + yield (name, dispname, objectType, docname, newestId, 1) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(CDomain) + app.add_config_value("c_id_attributes", [], 'env') + app.add_config_value("c_paren_attributes", [], 'env') + app.add_config_value("c_extra_keywords", _macroKeywords, 'env') + app.add_config_value("c_maximum_signature_line_length", None, 'env', types={int, None}) + app.add_post_transform(AliasTransform) + + return { + 'version': 'builtin', + 'env_version': 3, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py new file mode 100644 index 0000000..7cfe382 --- /dev/null +++ b/sphinx/domains/changeset.py @@ -0,0 +1,161 @@ +"""The changeset domain.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, NamedTuple, cast + +from docutils import nodes + +from sphinx import addnodes +from sphinx.domains import Domain +from sphinx.locale import _ +from sphinx.util.docutils import SphinxDirective + +if TYPE_CHECKING: + from docutils.nodes import Node + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + + +versionlabels = { + 'versionadded': _('New in version %s'), + 'versionchanged': _('Changed in version %s'), + 'deprecated': _('Deprecated since version %s'), +} + +versionlabel_classes = { + 'versionadded': 'added', + 'versionchanged': 'changed', + 'deprecated': 'deprecated', +} + + +class ChangeSet(NamedTuple): + type: str + docname: str + lineno: int + module: str | None + descname: str | None + content: str + + +class VersionChange(SphinxDirective): + """ + Directive to describe a change/addition/deprecation in a specific version. + """ + has_content = True + required_arguments = 1 + optional_arguments = 1 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + node = addnodes.versionmodified() + node.document = self.state.document + self.set_source_info(node) + node['type'] = self.name + node['version'] = self.arguments[0] + text = versionlabels[self.name] % self.arguments[0] + if len(self.arguments) == 2: + inodes, messages = self.state.inline_text(self.arguments[1], + self.lineno + 1) + para = nodes.paragraph(self.arguments[1], '', *inodes, translatable=False) + self.set_source_info(para) + node.append(para) + else: + messages = [] + if self.content: + self.state.nested_parse(self.content, self.content_offset, node) + classes = ['versionmodified', versionlabel_classes[self.name]] + if len(node) > 0 and isinstance(node[0], nodes.paragraph): + # the contents start with a paragraph + if node[0].rawsource: + # make the first paragraph translatable + content = nodes.inline(node[0].rawsource, translatable=True) + content.source = node[0].source + content.line = node[0].line + content += node[0].children + node[0].replace_self(nodes.paragraph('', '', content, translatable=False)) + + para = node[0] + para.insert(0, nodes.inline('', '%s: ' % text, classes=classes)) + elif len(node) > 0: + # the contents do not starts with a paragraph + para = nodes.paragraph('', '', + nodes.inline('', '%s: ' % text, classes=classes), + translatable=False) + node.insert(0, para) + else: + # the contents are empty + para = nodes.paragraph('', '', + nodes.inline('', '%s.' % text, classes=classes), + translatable=False) + node.append(para) + + domain = cast(ChangeSetDomain, self.env.get_domain('changeset')) + domain.note_changeset(node) + + ret: list[Node] = [node] + ret += messages + return ret + + +class ChangeSetDomain(Domain): + """Domain for changesets.""" + + name = 'changeset' + label = 'changeset' + + initial_data: dict[str, Any] = { + 'changes': {}, # version -> list of ChangeSet + } + + @property + def changesets(self) -> dict[str, list[ChangeSet]]: + return self.data.setdefault('changes', {}) # version -> list of ChangeSet + + def note_changeset(self, node: addnodes.versionmodified) -> None: + version = node['version'] + module = self.env.ref_context.get('py:module') + objname = self.env.temp_data.get('object') + changeset = ChangeSet(node['type'], self.env.docname, node.line, + module, objname, node.astext()) + self.changesets.setdefault(version, []).append(changeset) + + def clear_doc(self, docname: str) -> None: + for changes in self.changesets.values(): + for changeset in changes[:]: + if changeset.docname == docname: + changes.remove(changeset) + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX duplicates? + for version, otherchanges in otherdata['changes'].items(): + changes = self.changesets.setdefault(version, []) + for changeset in otherchanges: + if changeset.docname in docnames: + changes.append(changeset) + + def process_doc( + self, env: BuildEnvironment, docname: str, document: nodes.document, + ) -> None: + pass # nothing to do here. All changesets are registered on calling directive. + + def get_changesets_for(self, version: str) -> list[ChangeSet]: + return self.changesets.get(version, []) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(ChangeSetDomain) + app.add_directive('deprecated', VersionChange) + app.add_directive('versionadded', VersionChange) + app.add_directive('versionchanged', VersionChange) + + return { + 'version': 'builtin', + 'env_version': 1, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py new file mode 100644 index 0000000..d12c0f1 --- /dev/null +++ b/sphinx/domains/citation.py @@ -0,0 +1,154 @@ +"""The citation domain.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes + +from sphinx.addnodes import pending_xref +from sphinx.domains import Domain +from sphinx.locale import __ +from sphinx.transforms import SphinxTransform +from sphinx.util import logging +from sphinx.util.nodes import copy_source_info, make_refnode + +if TYPE_CHECKING: + from docutils.nodes import Element + + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + + +logger = logging.getLogger(__name__) + + +class CitationDomain(Domain): + """Domain for citations.""" + + name = 'citation' + label = 'citation' + + dangling_warnings = { + 'ref': 'citation not found: %(target)s', + } + + @property + def citations(self) -> dict[str, tuple[str, str, int]]: + return self.data.setdefault('citations', {}) + + @property + def citation_refs(self) -> dict[str, set[str]]: + return self.data.setdefault('citation_refs', {}) + + def clear_doc(self, docname: str) -> None: + for key, (fn, _l, _lineno) in list(self.citations.items()): + if fn == docname: + del self.citations[key] + for key, docnames in list(self.citation_refs.items()): + if docnames == {docname}: + del self.citation_refs[key] + elif docname in docnames: + docnames.remove(docname) + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX duplicates? + for key, data in otherdata['citations'].items(): + if data[0] in docnames: + self.citations[key] = data + for key, data in otherdata['citation_refs'].items(): + citation_refs = self.citation_refs.setdefault(key, set()) + for docname in data: + if docname in docnames: + citation_refs.add(docname) + + def note_citation(self, node: nodes.citation) -> None: + label = node[0].astext() + if label in self.citations: + path = self.env.doc2path(self.citations[label][0]) + logger.warning(__('duplicate citation %s, other instance in %s'), label, path, + location=node, type='ref', subtype='citation') + self.citations[label] = (node['docname'], node['ids'][0], node.line) + + def note_citation_reference(self, node: pending_xref) -> None: + docnames = self.citation_refs.setdefault(node['reftarget'], set()) + docnames.add(self.env.docname) + + def check_consistency(self) -> None: + for name, (docname, _labelid, lineno) in self.citations.items(): + if name not in self.citation_refs: + logger.warning(__('Citation [%s] is not referenced.'), name, + type='ref', subtype='citation', location=(docname, lineno)) + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + docname, labelid, lineno = self.citations.get(target, ('', '', 0)) + if not docname: + return None + + return make_refnode(builder, fromdocname, docname, + labelid, contnode) + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode) + if refnode is None: + return [] + else: + return [('ref', refnode)] + + +class CitationDefinitionTransform(SphinxTransform): + """Mark citation definition labels as not smartquoted.""" + default_priority = 619 + + def apply(self, **kwargs: Any) -> None: + domain = cast(CitationDomain, self.env.get_domain('citation')) + for node in self.document.findall(nodes.citation): + # register citation node to domain + node['docname'] = self.env.docname + domain.note_citation(node) + + # mark citation labels as not smartquoted + label = cast(nodes.label, node[0]) + label['support_smartquotes'] = False + + +class CitationReferenceTransform(SphinxTransform): + """ + Replace citation references by pending_xref nodes before the default + docutils transform tries to resolve them. + """ + default_priority = 619 + + def apply(self, **kwargs: Any) -> None: + domain = cast(CitationDomain, self.env.get_domain('citation')) + for node in self.document.findall(nodes.citation_reference): + target = node.astext() + ref = pending_xref(target, refdomain='citation', reftype='ref', + reftarget=target, refwarn=True, + support_smartquotes=False, + ids=node["ids"], + classes=node.get('classes', [])) + ref += nodes.inline(target, '[%s]' % target) + copy_source_info(node, ref) + node.replace_self(ref) + + # register reference node to domain + domain.note_citation_reference(ref) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(CitationDomain) + app.add_transform(CitationDefinitionTransform) + app.add_transform(CitationReferenceTransform) + + return { + 'version': 'builtin', + 'env_version': 1, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py new file mode 100644 index 0000000..80920c6 --- /dev/null +++ b/sphinx/domains/cpp.py @@ -0,0 +1,8233 @@ +"""The C++ language domain.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Any, Callable, TypeVar + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, ObjType +from sphinx.errors import NoUri +from sphinx.locale import _, __ +from sphinx.roles import SphinxRole, XRefRole +from sphinx.transforms import SphinxTransform +from sphinx.transforms.post_transforms import ReferencesResolver +from sphinx.util import logging +from sphinx.util.cfamily import ( + ASTAttributeList, + ASTBaseBase, + ASTBaseParenExprList, + BaseParser, + DefinitionError, + NoOldIdError, + StringifyTransform, + UnsupportedMultiCharacterCharLiteral, + anon_identifier_re, + binary_literal_re, + char_literal_re, + float_literal_re, + float_literal_suffix_re, + hex_literal_re, + identifier_re, + integer_literal_re, + integers_literal_suffix_re, + octal_literal_re, + verify_description_mode, +) +from sphinx.util.docfields import Field, GroupedField +from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import make_refnode + +if TYPE_CHECKING: + from collections.abc import Generator, Iterator + + from docutils.nodes import Element, Node, TextElement, system_message + + from sphinx.addnodes import desc_signature, pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + +logger = logging.getLogger(__name__) +T = TypeVar('T') + +""" + Important note on ids + ---------------------------------------------------------------------------- + + Multiple id generation schemes are used due to backwards compatibility. + - v1: 1.2.3 <= version < 1.3 + The style used before the rewrite. + It is not the actual old code, but a replication of the behaviour. + - v2: 1.3 <= version < now + Standardised mangling scheme from + https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling + though not completely implemented. + All versions are generated and attached to elements. The newest is used for + the index. All of the versions should work as permalinks. + + + Signature Nodes and Tagnames + ---------------------------------------------------------------------------- + + Each signature is in a desc_signature node, where all children are + desc_signature_line nodes. Each of these lines will have the attribute + 'sphinx_line_type' set to one of the following (prioritized): + - 'declarator', if the line contains the name of the declared object. + - 'templateParams', if the line starts a template parameter list, + - 'templateParams', if the line has template parameters + Note: such lines might get a new tag in the future. + - 'templateIntroduction, if the line is on the form 'conceptName{...}' + No other desc_signature nodes should exist (so far). + + + Grammar + ---------------------------------------------------------------------------- + + See https://www.nongnu.org/hcb/ for the grammar, + and https://github.com/cplusplus/draft/blob/master/source/grammar.tex, + and https://github.com/cplusplus/concepts-ts + for the newest grammar. + + common grammar things: + template-declaration -> + "template" "<" template-parameter-list ">" declaration + template-parameter-list -> + template-parameter + | template-parameter-list "," template-parameter + template-parameter -> + type-parameter + | parameter-declaration # i.e., same as a function argument + + type-parameter -> + "class" "..."[opt] identifier[opt] + | "class" identifier[opt] "=" type-id + | "typename" "..."[opt] identifier[opt] + | "typename" identifier[opt] "=" type-id + | "template" "<" template-parameter-list ">" + "class" "..."[opt] identifier[opt] + | "template" "<" template-parameter-list ">" + "class" identifier[opt] "=" id-expression + # also, from C++17 we can have "typename" in template templates + templateDeclPrefix -> + "template" "<" template-parameter-list ">" + + 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 + -> decl-specifier-seq declarator initializer + + decl-specifier -> + storage-class-specifier -> + ( "static" (only for member_object and function_object) + | "extern" (only for member_object and function_object) + | "register" + ) + thread_local[opt] (only for member_object) + (it can also appear before the others) + + | type-specifier -> trailing-type-specifier + | function-specifier -> "inline" | "virtual" | "explicit" (only + for function_object) + | "friend" (only for function_object) + | "constexpr" (only for member_object and function_object) + trailing-type-specifier -> + simple-type-specifier + | elaborated-type-specifier + | typename-specifier + | cv-qualifier -> "const" | "volatile" + stricter grammar for decl-specifier-seq (with everything, each object + uses a subset): + visibility storage-class-specifier function-specifier "friend" + "constexpr" "volatile" "const" trailing-type-specifier + # where trailing-type-specifier can no be cv-qualifier + # Inside e.g., template parameters a strict subset is used + # (see type-specifier-seq) + trailing-type-specifier -> + simple-type-specifier -> + ::[opt] nested-name-specifier[opt] type-name + | ::[opt] nested-name-specifier "template" simple-template-id + | "char" | "bool" | etc. + | decltype-specifier + | elaborated-type-specifier -> + class-key attribute-specifier-seq[opt] ::[opt] + nested-name-specifier[opt] identifier + | class-key ::[opt] nested-name-specifier[opt] template[opt] + simple-template-id + | "enum" ::[opt] nested-name-specifier[opt] identifier + | typename-specifier -> + "typename" ::[opt] nested-name-specifier identifier + | "typename" ::[opt] nested-name-specifier template[opt] + simple-template-id + class-key -> "class" | "struct" | "union" + type-name ->* identifier | simple-template-id + # ignoring attributes and decltype, and then some left-factoring + trailing-type-specifier -> + rest-of-trailing + ("class" | "struct" | "union" | "typename") rest-of-trailing + built-in -> "char" | "bool" | etc. + decltype-specifier + rest-of-trailing -> (with some simplification) + "::"[opt] list-of-elements-separated-by-:: + element -> + "template"[opt] identifier ("<" template-argument-list ">")[opt] + template-argument-list -> + template-argument "..."[opt] + | template-argument-list "," template-argument "..."[opt] + template-argument -> + constant-expression + | type-specifier-seq abstract-declarator + | id-expression + + + declarator -> + ptr-declarator + | noptr-declarator parameters-and-qualifiers trailing-return-type + ptr-declarator -> + noptr-declarator + | ptr-operator ptr-declarator + noptr-declarator -> + declarator-id attribute-specifier-seq[opt] -> + "..."[opt] id-expression + | rest-of-trailing + | noptr-declarator parameters-and-qualifiers + | noptr-declarator "[" constant-expression[opt] "]" + attribute-specifier-seq[opt] + | "(" ptr-declarator ")" + ptr-operator -> + "*" attribute-specifier-seq[opt] cv-qualifier-seq[opt] + | "& attribute-specifier-seq[opt] + | "&&" attribute-specifier-seq[opt] + | "::"[opt] nested-name-specifier "*" attribute-specifier-seq[opt] + cv-qualifier-seq[opt] + # function_object must use a parameters-and-qualifiers, the others may + # use it (e.g., function pointers) + parameters-and-qualifiers -> + "(" parameter-clause ")" attribute-specifier-seq[opt] + cv-qualifier-seq[opt] ref-qualifier[opt] + exception-specification[opt] + ref-qualifier -> "&" | "&&" + exception-specification -> + "noexcept" ("(" constant-expression ")")[opt] + "throw" ("(" type-id-list ")")[opt] + # TODO: we don't implement attributes + # member functions can have initializers, but we fold them into here + memberFunctionInit -> "=" "0" + # (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. + + concept_object: + goal: + just a declaration of the name (for now) + + grammar: only a single template parameter list, and the nested name + may not have any template argument lists + + "template" "<" template-parameter-list ">" + nested-name-specifier + + type_object: + goal: + either a single type (e.g., "MyClass:Something_T" or a typedef-like + thing (e.g. "Something Something_T" or "int I_arr[]" + grammar, single type: based on a type in a function parameter, but + without a name: + parameter-declaration + -> attribute-specifier-seq[opt] decl-specifier-seq + abstract-declarator[opt] + # Drop the attributes + -> decl-specifier-seq abstract-declarator[opt] + grammar, typedef-like: no initilizer + decl-specifier-seq declarator + Can start with a templateDeclPrefix. + + member_object: + goal: as a type_object which must have a declarator, and optionally + with a initializer + grammar: + decl-specifier-seq declarator initializer + Can start with a templateDeclPrefix. + + function_object: + goal: a function declaration, TODO: what about templates? for now: skip + grammar: no initializer + decl-specifier-seq declarator + Can start with a templateDeclPrefix. + + class_object: + goal: a class declaration, but with specification of a base class + grammar: + attribute-specifier-seq[opt] + nested-name "final"[opt] (":" base-specifier-list)[opt] + base-specifier-list -> + base-specifier "..."[opt] + | base-specifier-list, base-specifier "..."[opt] + base-specifier -> + base-type-specifier + | "virtual" access-spe"cifier[opt] base-type-specifier + | access-specifier[opt] "virtual"[opt] base-type-specifier + Can start with a templateDeclPrefix. + + enum_object: + goal: an unscoped enum or a scoped enum, optionally with the underlying + type specified + grammar: + ("class" | "struct")[opt] visibility[opt] + attribute-specifier-seq[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 +""" + +udl_identifier_re = re.compile(r''' + [a-zA-Z_][a-zA-Z0-9_]*\b # note, no word boundary in the beginning +''', re.VERBOSE) +_string_re = re.compile(r"[LuU8]?('([^'\\]*(?:\\.[^'\\]*)*)'" + r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) +_visibility_re = re.compile(r'\b(public|private|protected)\b') +_operator_re = re.compile(r''' + \[\s*\] + | \(\s*\) + | \+\+ | -- + | ->\*? | \, + | (<<|>>)=? | && | \|\| + | <=> + | [!<>=/*%+|&^~-]=? + | (\b(and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|xor|xor_eq)\b) +''', re.VERBOSE) +_fold_operator_re = re.compile(r''' + ->\* | \.\* | \, + | (<<|>>)=? | && | \|\| + | != + | [<>=/*%+|&^~-]=? +''', re.VERBOSE) +# see https://en.cppreference.com/w/cpp/keyword +_keywords = [ + 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', + 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', + 'class', 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', + 'const_cast', 'continue', + 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', + 'enum', 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', + 'goto', 'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new', + 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', + 'private', 'protected', 'public', 'register', 'reinterpret_cast', + 'requires', 'return', 'short', 'signed', 'sizeof', 'static', + 'static_assert', 'static_cast', 'struct', 'switch', 'template', 'this', + 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', + 'union', 'unsigned', 'using', 'virtual', 'void', 'volatile', 'wchar_t', + 'while', 'xor', 'xor_eq', +] + + +_simple_type_specifiers_re = re.compile(r""" + \b( + auto|void|bool + |signed|unsigned + |short|long + |char|wchar_t|char(8|16|32)_t + |int + |__int(64|128) # extension + |float|double + |__float80|_Float64x|__float128|_Float128 # extension + |_Complex|_Imaginary # extension + )\b +""", re.VERBOSE) + +_max_id = 4 +_id_prefix = [None, '', '_CPPv2', '_CPPv3', '_CPPv4'] +# Ids are used in lookup keys which are used across pickled files, +# so when _max_id changes, make sure to update the ENV_VERSION. + +# ------------------------------------------------------------------------------ +# Id v1 constants +# ------------------------------------------------------------------------------ + +_id_fundamental_v1 = { + 'char': 'c', + 'signed char': 'c', + 'unsigned char': 'C', + 'int': 'i', + 'signed int': 'i', + 'unsigned int': 'U', + 'long': 'l', + 'signed long': 'l', + 'unsigned long': 'L', + 'bool': 'b', +} +_id_shorthands_v1 = { + 'std::string': 'ss', + 'std::ostream': 'os', + 'std::istream': 'is', + 'std::iostream': 'ios', + 'std::vector': 'v', + 'std::map': 'm', +} +_id_operator_v1 = { + 'new': 'new-operator', + 'new[]': 'new-array-operator', + 'delete': 'delete-operator', + 'delete[]': 'delete-array-operator', + # the arguments will make the difference between unary and binary + # '+(unary)' : 'ps', + # '-(unary)' : 'ng', + # '&(unary)' : 'ad', + # '*(unary)' : 'de', + '~': 'inv-operator', + '+': 'add-operator', + '-': 'sub-operator', + '*': 'mul-operator', + '/': 'div-operator', + '%': 'mod-operator', + '&': 'and-operator', + '|': 'or-operator', + '^': 'xor-operator', + '=': 'assign-operator', + '+=': 'add-assign-operator', + '-=': 'sub-assign-operator', + '*=': 'mul-assign-operator', + '/=': 'div-assign-operator', + '%=': 'mod-assign-operator', + '&=': 'and-assign-operator', + '|=': 'or-assign-operator', + '^=': 'xor-assign-operator', + '<<': 'lshift-operator', + '>>': 'rshift-operator', + '<<=': 'lshift-assign-operator', + '>>=': 'rshift-assign-operator', + '==': 'eq-operator', + '!=': 'neq-operator', + '<': 'lt-operator', + '>': 'gt-operator', + '<=': 'lte-operator', + '>=': 'gte-operator', + '!': 'not-operator', + '&&': 'sand-operator', + '||': 'sor-operator', + '++': 'inc-operator', + '--': 'dec-operator', + ',': 'comma-operator', + '->*': 'pointer-by-pointer-operator', + '->': 'pointer-operator', + '()': 'call-operator', + '[]': 'subscript-operator', +} + +# ------------------------------------------------------------------------------ +# Id v > 1 constants +# ------------------------------------------------------------------------------ + +_id_fundamental_v2 = { + # not all of these are actually parsed as fundamental types, TODO: do that + 'void': 'v', + 'bool': 'b', + 'char': 'c', + 'signed char': 'a', + 'unsigned char': 'h', + 'wchar_t': 'w', + 'char32_t': 'Di', + 'char16_t': 'Ds', + 'char8_t': 'Du', + 'short': 's', + 'short int': 's', + 'signed short': 's', + 'signed short int': 's', + 'unsigned short': 't', + 'unsigned short int': 't', + 'int': 'i', + 'signed': 'i', + 'signed int': 'i', + 'unsigned': 'j', + 'unsigned int': 'j', + 'long': 'l', + 'long int': 'l', + 'signed long': 'l', + 'signed long int': 'l', + 'unsigned long': 'm', + 'unsigned long int': 'm', + 'long long': 'x', + 'long long int': 'x', + 'signed long long': 'x', + 'signed long long int': 'x', + '__int64': 'x', + 'unsigned long long': 'y', + 'unsigned long long int': 'y', + '__int128': 'n', + 'signed __int128': 'n', + 'unsigned __int128': 'o', + 'float': 'f', + 'double': 'd', + 'long double': 'e', + '__float80': 'e', '_Float64x': 'e', + '__float128': 'g', '_Float128': 'g', + '_Complex float': 'Cf', + '_Complex double': 'Cd', + '_Complex long double': 'Ce', + '_Imaginary float': 'f', + '_Imaginary double': 'd', + '_Imaginary long double': 'e', + 'auto': 'Da', + 'decltype(auto)': 'Dc', + 'std::nullptr_t': 'Dn', +} +_id_operator_v2 = { + 'new': 'nw', + 'new[]': 'na', + 'delete': 'dl', + 'delete[]': 'da', + # the arguments will make the difference between unary and binary + # in operator definitions + # '+(unary)' : 'ps', + # '-(unary)' : 'ng', + # '&(unary)' : 'ad', + # '*(unary)' : 'de', + '~': 'co', 'compl': 'co', + '+': 'pl', + '-': 'mi', + '*': 'ml', + '/': 'dv', + '%': 'rm', + '&': 'an', 'bitand': 'an', + '|': 'or', 'bitor': 'or', + '^': 'eo', 'xor': 'eo', + '=': 'aS', + '+=': 'pL', + '-=': 'mI', + '*=': 'mL', + '/=': 'dV', + '%=': 'rM', + '&=': 'aN', 'and_eq': 'aN', + '|=': 'oR', 'or_eq': 'oR', + '^=': 'eO', 'xor_eq': 'eO', + '<<': 'ls', + '>>': 'rs', + '<<=': 'lS', + '>>=': 'rS', + '==': 'eq', + '!=': 'ne', 'not_eq': 'ne', + '<': 'lt', + '>': 'gt', + '<=': 'le', + '>=': 'ge', + '<=>': 'ss', + '!': 'nt', 'not': 'nt', + '&&': 'aa', 'and': 'aa', + '||': 'oo', 'or': 'oo', + '++': 'pp', + '--': 'mm', + ',': 'cm', + '->*': 'pm', + '->': 'pt', + '()': 'cl', + '[]': 'ix', + '.*': 'ds', # this one is not overloadable, but we need it for expressions + '?': 'qu', +} +_id_operator_unary_v2 = { + '++': 'pp_', + '--': 'mm_', + '*': 'de', + '&': 'ad', + '+': 'ps', + '-': 'ng', + '!': 'nt', 'not': 'nt', + '~': 'co', 'compl': 'co', +} +_id_char_from_prefix: dict[str | None, str] = { + None: 'c', 'u8': 'c', + 'u': 'Ds', 'U': 'Di', 'L': 'w', +} +# these are ordered by preceedence +_expression_bin_ops = [ + ['||', 'or'], + ['&&', 'and'], + ['|', 'bitor'], + ['^', 'xor'], + ['&', 'bitand'], + ['==', '!=', 'not_eq'], + ['<=>', '<=', '>=', '<', '>'], + ['<<', '>>'], + ['+', '-'], + ['*', '/', '%'], + ['.*', '->*'], +] +_expression_unary_ops = ["++", "--", "*", "&", "+", "-", "!", "not", "~", "compl"] +_expression_assignment_ops = ["=", "*=", "/=", "%=", "+=", "-=", + ">>=", "<<=", "&=", "and_eq", "^=", "|=", "xor_eq", "or_eq"] +_id_explicit_cast = { + 'dynamic_cast': 'dc', + 'static_cast': 'sc', + 'const_cast': 'cc', + 'reinterpret_cast': 'rc', +} + + +class _DuplicateSymbolError(Exception): + def __init__(self, symbol: Symbol, declaration: ASTDeclaration) -> None: + assert symbol + assert declaration + self.symbol = symbol + self.declaration = declaration + + def __str__(self) -> str: + return "Internal C++ duplicate symbol error:\n%s" % self.symbol.dump(0) + + +class ASTBase(ASTBaseBase): + pass + + +# Names +################################################################################ + +class ASTIdentifier(ASTBase): + def __init__(self, identifier: str) -> None: + assert identifier is not None + assert len(identifier) != 0 + self.identifier = identifier + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.identifier) + + def is_anon(self) -> bool: + return self.identifier[0] == '@' + + def get_id(self, version: int) -> str: + if self.is_anon() and version < 3: + raise NoOldIdError + if version == 1: + if self.identifier == 'size_t': + return 's' + else: + return self.identifier + if self.identifier == "std": + return 'St' + elif self.identifier[0] == "~": + # a destructor, just use an arbitrary version of dtors + return 'D0' + else: + if self.is_anon(): + return 'Ut%d_%s' % (len(self.identifier) - 1, self.identifier[1:]) + else: + return str(len(self.identifier)) + self.identifier + + # and this is where we finally make a difference between __str__ and the display string + + def __str__(self) -> str: + return self.identifier + + def get_display_string(self) -> str: + return "[anonymous]" if self.is_anon() else self.identifier + + def describe_signature(self, signode: TextElement, mode: str, env: BuildEnvironment, + prefix: str, templateArgs: str, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.is_anon(): + node = addnodes.desc_sig_name(text="[anonymous]") + else: + node = addnodes.desc_sig_name(self.identifier, self.identifier) + if mode == 'markType': + targetText = prefix + self.identifier + templateArgs + pnode = addnodes.pending_xref('', refdomain='cpp', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + pnode['cpp:parent_key'] = symbol.get_lookup_key() + pnode += node + signode += pnode + elif mode == 'lastIsName': + nameNode = addnodes.desc_name() + nameNode += node + signode += nameNode + elif mode == 'noneIsName': + signode += node + elif mode == 'param': + node['classes'].append('sig-param') + signode += node + elif mode == 'udl': + # the target is 'operator""id' instead of just 'id' + assert len(prefix) == 0 + assert len(templateArgs) == 0 + assert not self.is_anon() + targetText = 'operator""' + self.identifier + pnode = addnodes.pending_xref('', refdomain='cpp', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + pnode['cpp:parent_key'] = symbol.get_lookup_key() + pnode += node + signode += pnode + else: + raise Exception('Unknown description mode: %s' % mode) + + +class ASTNestedNameElement(ASTBase): + def __init__(self, identOrOp: ASTIdentifier | ASTOperator, + templateArgs: ASTTemplateArgs) -> None: + self.identOrOp = identOrOp + self.templateArgs = templateArgs + + def is_operator(self) -> bool: + return False + + def get_id(self, version: int) -> str: + res = self.identOrOp.get_id(version) + if self.templateArgs: + res += self.templateArgs.get_id(version) + return res + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.identOrOp) + if self.templateArgs: + res += transform(self.templateArgs) + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, prefix: str, symbol: Symbol) -> None: + tArgs = str(self.templateArgs) if self.templateArgs is not None else '' + self.identOrOp.describe_signature(signode, mode, env, prefix, tArgs, symbol) + if self.templateArgs is not None: + self.templateArgs.describe_signature(signode, 'markType', env, symbol) + + +class ASTNestedName(ASTBase): + def __init__(self, names: list[ASTNestedNameElement], + templates: list[bool], rooted: bool) -> None: + assert len(names) > 0 + self.names = names + self.templates = templates + assert len(self.names) == len(self.templates) + self.rooted = rooted + + @property + def name(self) -> ASTNestedName: + return self + + def num_templates(self) -> int: + count = 0 + for n in self.names: + if n.is_operator(): + continue + if n.templateArgs: + count += 1 + return count + + def get_id(self, version: int, modifiers: str = '') -> str: + if version == 1: + tt = str(self) + if tt in _id_shorthands_v1: + return _id_shorthands_v1[tt] + else: + return '::'.join(n.get_id(version) for n in self.names) + + res = [] + if len(self.names) > 1 or len(modifiers) > 0: + res.append('N') + res.append(modifiers) + for n in self.names: + res.append(n.get_id(version)) + if len(self.names) > 1 or len(modifiers) > 0: + res.append('E') + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.rooted: + res.append('') + for i in range(len(self.names)): + n = self.names[i] + if self.templates[i]: + res.append("template " + transform(n)) + else: + res.append(transform(n)) + return '::'.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + # just print the name part, with template args, not template params + if mode == 'noneIsName': + if self.rooted: + unreachable = "Can this happen?" + raise AssertionError(unreachable) # TODO + signode += nodes.Text('::') + for i in range(len(self.names)): + if i != 0: + unreachable = "Can this happen?" + raise AssertionError(unreachable) # TODO + signode += nodes.Text('::blah') + n = self.names[i] + if self.templates[i]: + unreachable = "Can this happen?" + raise AssertionError(unreachable) # TODO + signode += nodes.Text("template") + signode += nodes.Text(" ") + n.describe_signature(signode, mode, env, '', symbol) + elif mode == 'param': + assert not self.rooted, str(self) + assert len(self.names) == 1 + assert not self.templates[0] + self.names[0].describe_signature(signode, 'param', env, '', symbol) + elif mode in ('markType', 'lastIsName', 'markName'): + # Each element should be a pending xref targeting the complete + # prefix. however, only the identifier part should be a link, such + # that template args can be a link as well. + # For 'lastIsName' we should also prepend template parameter lists. + templateParams: list[Any] = [] + if mode == 'lastIsName': + assert symbol is not None + if symbol.declaration.templatePrefix is not None: + templateParams = symbol.declaration.templatePrefix.templates + iTemplateParams = 0 + templateParamsPrefix = '' + prefix = '' + first = True + names = self.names[:-1] if mode == 'lastIsName' else self.names + # If lastIsName, then wrap all of the prefix in a desc_addname, + # else append directly to signode. + # NOTE: Breathe previously relied on the prefix being in the desc_addname node, + # so it can remove it in inner declarations. + dest = signode + if mode == 'lastIsName': + dest = addnodes.desc_addname() + if self.rooted: + prefix += '::' + if mode == 'lastIsName' and len(names) == 0: + signode += addnodes.desc_sig_punctuation('::', '::') + else: + dest += addnodes.desc_sig_punctuation('::', '::') + for i in range(len(names)): + nne = names[i] + template = self.templates[i] + if not first: + dest += addnodes.desc_sig_punctuation('::', '::') + prefix += '::' + if template: + dest += addnodes.desc_sig_keyword('template', 'template') + dest += addnodes.desc_sig_space() + first = False + txt_nne = str(nne) + if txt_nne != '': + if nne.templateArgs and iTemplateParams < len(templateParams): + templateParamsPrefix += str(templateParams[iTemplateParams]) + iTemplateParams += 1 + nne.describe_signature(dest, 'markType', + env, templateParamsPrefix + prefix, symbol) + prefix += txt_nne + if mode == 'lastIsName': + if len(self.names) > 1: + dest += addnodes.desc_sig_punctuation('::', '::') + signode += dest + if self.templates[-1]: + signode += addnodes.desc_sig_keyword('template', 'template') + signode += addnodes.desc_sig_space() + self.names[-1].describe_signature(signode, mode, env, '', symbol) + else: + raise Exception('Unknown description mode: %s' % mode) + + +################################################################################ +# Expressions +################################################################################ + +class ASTExpression(ASTBase): + def get_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + +# Primary expressions +################################################################################ + +class ASTLiteral(ASTExpression): + pass + + +class ASTPointerLiteral(ASTLiteral): + def _stringify(self, transform: StringifyTransform) -> str: + return 'nullptr' + + def get_id(self, version: int) -> str: + return 'LDnE' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('nullptr', 'nullptr') + + +class ASTBooleanLiteral(ASTLiteral): + def __init__(self, value: bool) -> None: + self.value = value + + def _stringify(self, transform: StringifyTransform) -> str: + if self.value: + return 'true' + else: + return 'false' + + def get_id(self, version: int) -> str: + if self.value: + return 'L1E' + else: + return 'L0E' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword(str(self), str(self)) + + +class ASTNumberLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def get_id(self, version: int) -> str: + # TODO: floats should be mangled by writing the hex of the binary representation + return "L%sE" % self.data.replace("'", "") + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_literal_number(self.data, self.data) + + +class ASTStringLiteral(ASTLiteral): + def __init__(self, data: str) -> None: + self.data = data + + def _stringify(self, transform: StringifyTransform) -> str: + return self.data + + def get_id(self, version: int) -> str: + # note: the length is not really correct with escaping + return "LA%d_KcE" % (len(self.data) - 2) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_literal_string(self.data, self.data) + + +class ASTCharLiteral(ASTLiteral): + def __init__(self, prefix: str, data: str) -> None: + self.prefix = prefix # may be None when no prefix + self.data = data + assert prefix in _id_char_from_prefix + self.type = _id_char_from_prefix[prefix] + decoded = data.encode().decode('unicode-escape') + if len(decoded) == 1: + self.value = ord(decoded) + else: + raise UnsupportedMultiCharacterCharLiteral(decoded) + + def _stringify(self, transform: StringifyTransform) -> str: + if self.prefix is None: + return "'" + self.data + "'" + else: + return self.prefix + "'" + self.data + "'" + + def get_id(self, version: int) -> str: + # TODO: the ID should be have L E around it + return self.type + str(self.value) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.prefix is not None: + signode += addnodes.desc_sig_keyword(self.prefix, self.prefix) + txt = "'" + self.data + "'" + signode += addnodes.desc_sig_literal_char(txt, txt) + + +class ASTUserDefinedLiteral(ASTLiteral): + def __init__(self, literal: ASTLiteral, ident: ASTIdentifier): + self.literal = literal + self.ident = ident + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.literal) + transform(self.ident) + + def get_id(self, version: int) -> str: + # mangle as if it was a function call: ident(literal) + return f'clL_Zli{self.ident.get_id(version)}E{self.literal.get_id(version)}E' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.literal.describe_signature(signode, mode, env, symbol) + self.ident.describe_signature(signode, "udl", env, "", "", symbol) + + +################################################################################ + +class ASTThisLiteral(ASTExpression): + def _stringify(self, transform: StringifyTransform) -> str: + return "this" + + def get_id(self, version: int) -> str: + return "fpT" + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('this', 'this') + + +class ASTFoldExpr(ASTExpression): + def __init__(self, leftExpr: ASTExpression, + op: str, rightExpr: ASTExpression) -> None: + assert leftExpr is not None or rightExpr is not None + self.leftExpr = leftExpr + self.op = op + self.rightExpr = rightExpr + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + if self.leftExpr: + res.append(transform(self.leftExpr)) + res.append(' ') + res.append(self.op) + res.append(' ') + res.append('...') + if self.rightExpr: + res.append(' ') + res.append(self.op) + res.append(' ') + res.append(transform(self.rightExpr)) + res.append(')') + return ''.join(res) + + def get_id(self, version: int) -> str: + assert version >= 3 + if version == 3: + return str(self) + # https://github.com/itanium-cxx-abi/cxx-abi/pull/67 + res = [] + if self.leftExpr is None: # (... op expr) + res.append('fl') + elif self.rightExpr is None: # (expr op ...) + res.append('fr') + else: # (expr op ... op expr) + # we don't check where the parameter pack is, + # we just always call this a binary left fold + res.append('fL') + res.append(_id_operator_v2[self.op]) + if self.leftExpr: + res.append(self.leftExpr.get_id(version)) + if self.rightExpr: + res.append(self.rightExpr.get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('(', '(') + if self.leftExpr: + self.leftExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_operator(self.op, self.op) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('...', '...') + if self.rightExpr: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_operator(self.op, self.op) + signode += addnodes.desc_sig_space() + self.rightExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTParenExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '(' + transform(self.expr) + ')' + + def get_id(self, version: int) -> str: + return self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTIdExpression(ASTExpression): + def __init__(self, name: ASTNestedName): + # note: this class is basically to cast a nested name as an expression + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.name) + + def get_id(self, version: int) -> str: + return self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.name.describe_signature(signode, mode, env, symbol) + + +# Postfix expressions +################################################################################ + +class ASTPostfixOp(ASTBase): + def get_id(self, idPrefix: str, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + +class ASTPostfixArray(ASTPostfixOp): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return '[' + transform(self.expr) + ']' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'ix' + idPrefix + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('[', '[') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(']', ']') + + +class ASTPostfixMember(ASTPostfixOp): + def __init__(self, name: ASTNestedName): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '.' + transform(self.name) + + def get_id(self, idPrefix: str, version: int) -> str: + return 'dt' + idPrefix + self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('.', '.') + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixMemberOfPointer(ASTPostfixOp): + def __init__(self, name: ASTNestedName): + self.name = name + + def _stringify(self, transform: StringifyTransform) -> str: + return '->' + transform(self.name) + + def get_id(self, idPrefix: str, version: int) -> str: + return 'pt' + idPrefix + self.name.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_operator('->', '->') + self.name.describe_signature(signode, 'noneIsName', env, symbol) + + +class ASTPostfixInc(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '++' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'pp' + idPrefix + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_operator('++', '++') + + +class ASTPostfixDec(ASTPostfixOp): + def _stringify(self, transform: StringifyTransform) -> str: + return '--' + + def get_id(self, idPrefix: str, version: int) -> str: + return 'mm' + idPrefix + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_operator('--', '--') + + +class ASTPostfixCallExpr(ASTPostfixOp): + def __init__(self, lst: ASTParenExprList | ASTBracedInitList) -> None: + self.lst = lst + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.lst) + + def get_id(self, idPrefix: str, version: int) -> str: + res = ['cl', idPrefix] + for e in self.lst.exprs: + res.append(e.get_id(version)) + res.append('E') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.lst.describe_signature(signode, mode, env, symbol) + + +class ASTPostfixExpr(ASTExpression): + def __init__(self, prefix: ASTType, postFixes: list[ASTPostfixOp]): + self.prefix = prefix + self.postFixes = postFixes + + def _stringify(self, transform: StringifyTransform) -> str: + res = [transform(self.prefix)] + for p in self.postFixes: + res.append(transform(p)) + return ''.join(res) + + def get_id(self, version: int) -> str: + id = self.prefix.get_id(version) + for p in self.postFixes: + id = p.get_id(id, version) + return id + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.prefix.describe_signature(signode, mode, env, symbol) + for p in self.postFixes: + p.describe_signature(signode, mode, env, symbol) + + +class ASTExplicitCast(ASTExpression): + def __init__(self, cast: str, typ: ASTType, expr: ASTExpression): + assert cast in _id_explicit_cast + self.cast = cast + self.typ = typ + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = [self.cast] + res.append('<') + res.append(transform(self.typ)) + res.append('>(') + res.append(transform(self.expr)) + res.append(')') + return ''.join(res) + + def get_id(self, version: int) -> str: + return (_id_explicit_cast[self.cast] + + self.typ.get_id(version) + + self.expr.get_id(version)) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword(self.cast, self.cast) + signode += addnodes.desc_sig_punctuation('<', '<') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation('>', '>') + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTTypeId(ASTExpression): + def __init__(self, typeOrExpr: ASTType | ASTExpression, isType: bool): + self.typeOrExpr = typeOrExpr + self.isType = isType + + def _stringify(self, transform: StringifyTransform) -> str: + return 'typeid(' + transform(self.typeOrExpr) + ')' + + def get_id(self, version: int) -> str: + prefix = 'ti' if self.isType else 'te' + return prefix + self.typeOrExpr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('typeid', 'typeid') + signode += addnodes.desc_sig_punctuation('(', '(') + self.typeOrExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +# Unary expressions +################################################################################ + +class ASTUnaryOpExpr(ASTExpression): + def __init__(self, op: str, expr: ASTExpression): + self.op = op + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + if self.op[0] in 'cn': + return self.op + " " + transform(self.expr) + else: + return self.op + transform(self.expr) + + def get_id(self, version: int) -> str: + return _id_operator_unary_v2[self.op] + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.op[0] in 'cn': + signode += addnodes.desc_sig_keyword(self.op, self.op) + signode += addnodes.desc_sig_space() + else: + signode += addnodes.desc_sig_operator(self.op, self.op) + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTSizeofParamPack(ASTExpression): + def __init__(self, identifier: ASTIdentifier): + self.identifier = identifier + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof...(" + transform(self.identifier) + ")" + + def get_id(self, version: int) -> str: + return 'sZ' + self.identifier.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('sizeof', 'sizeof') + signode += addnodes.desc_sig_punctuation('...', '...') + signode += addnodes.desc_sig_punctuation('(', '(') + self.identifier.describe_signature(signode, 'markType', env, + symbol=symbol, prefix="", templateArgs="") + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTSizeofType(ASTExpression): + def __init__(self, typ: ASTType): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof(" + transform(self.typ) + ")" + + def get_id(self, version: int) -> str: + return 'st' + self.typ.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('sizeof', 'sizeof') + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTSizeofExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return "sizeof " + transform(self.expr) + + def get_id(self, version: int) -> str: + return 'sz' + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('sizeof', 'sizeof') + signode += addnodes.desc_sig_space() + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTAlignofExpr(ASTExpression): + def __init__(self, typ: ASTType): + self.typ = typ + + def _stringify(self, transform: StringifyTransform) -> str: + return "alignof(" + transform(self.typ) + ")" + + def get_id(self, version: int) -> str: + return 'at' + self.typ.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('alignof', 'alignof') + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTNoexceptExpr(ASTExpression): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return 'noexcept(' + transform(self.expr) + ')' + + def get_id(self, version: int) -> str: + return 'nx' + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('noexcept', 'noexcept') + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTNewExpr(ASTExpression): + def __init__(self, rooted: bool, isNewTypeId: bool, typ: ASTType, + initList: ASTParenExprList | ASTBracedInitList) -> None: + self.rooted = rooted + self.isNewTypeId = isNewTypeId + self.typ = typ + self.initList = initList + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.rooted: + res.append('::') + res.append('new ') + # TODO: placement + if self.isNewTypeId: + res.append(transform(self.typ)) + else: + raise AssertionError + if self.initList is not None: + res.append(transform(self.initList)) + return ''.join(res) + + def get_id(self, version: int) -> str: + # the array part will be in the type mangling, so na is not used + res = ['nw'] + # TODO: placement + res.append('_') + res.append(self.typ.get_id(version)) + if self.initList is not None: + res.append(self.initList.get_id(version)) + else: + res.append('E') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.rooted: + signode += addnodes.desc_sig_punctuation('::', '::') + signode += addnodes.desc_sig_keyword('new', 'new') + signode += addnodes.desc_sig_space() + # TODO: placement + if self.isNewTypeId: + self.typ.describe_signature(signode, mode, env, symbol) + else: + raise AssertionError + if self.initList is not None: + self.initList.describe_signature(signode, mode, env, symbol) + + +class ASTDeleteExpr(ASTExpression): + def __init__(self, rooted: bool, array: bool, expr: ASTExpression): + self.rooted = rooted + self.array = array + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.rooted: + res.append('::') + res.append('delete ') + if self.array: + res.append('[] ') + res.append(transform(self.expr)) + return ''.join(res) + + def get_id(self, version: int) -> str: + if self.array: + id = "da" + else: + id = "dl" + return id + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.rooted: + signode += addnodes.desc_sig_punctuation('::', '::') + signode += addnodes.desc_sig_keyword('delete', 'delete') + signode += addnodes.desc_sig_space() + if self.array: + signode += addnodes.desc_sig_punctuation('[]', '[]') + signode += addnodes.desc_sig_space() + self.expr.describe_signature(signode, mode, env, symbol) + + +# Other expressions +################################################################################ + +class ASTCastExpr(ASTExpression): + def __init__(self, typ: ASTType, expr: ASTExpression): + self.typ = typ + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.typ)) + res.append(')') + res.append(transform(self.expr)) + return ''.join(res) + + def get_id(self, version: int) -> str: + return 'cv' + self.typ.get_id(version) + self.expr.get_id(version) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_punctuation('(', '(') + self.typ.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + self.expr.describe_signature(signode, mode, env, symbol) + + +class ASTBinOpExpr(ASTExpression): + def __init__(self, exprs: list[ASTExpression], ops: list[str]): + assert len(exprs) > 0 + assert len(exprs) == len(ops) + 1 + self.exprs = exprs + self.ops = ops + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.exprs[0])) + for i in range(1, len(self.exprs)): + res.append(' ') + res.append(self.ops[i - 1]) + res.append(' ') + res.append(transform(self.exprs[i])) + return ''.join(res) + + def get_id(self, version: int) -> str: + assert version >= 2 + res = [] + for i in range(len(self.ops)): + res.append(_id_operator_v2[self.ops[i]]) + res.append(self.exprs[i].get_id(version)) + res.append(self.exprs[-1].get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode += addnodes.desc_sig_space() + op = self.ops[i - 1] + if ord(op[0]) >= ord('a') and ord(op[0]) <= ord('z'): + signode += addnodes.desc_sig_keyword(op, op) + else: + signode += addnodes.desc_sig_operator(op, op) + signode += addnodes.desc_sig_space() + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTConditionalExpr(ASTExpression): + def __init__(self, ifExpr: ASTExpression, thenExpr: ASTExpression, + elseExpr: ASTExpression): + self.ifExpr = ifExpr + self.thenExpr = thenExpr + self.elseExpr = elseExpr + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.ifExpr)) + res.append(' ? ') + res.append(transform(self.thenExpr)) + res.append(' : ') + res.append(transform(self.elseExpr)) + return ''.join(res) + + def get_id(self, version: int) -> str: + assert version >= 2 + res = [] + res.append(_id_operator_v2['?']) + res.append(self.ifExpr.get_id(version)) + res.append(self.thenExpr.get_id(version)) + res.append(self.elseExpr.get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.ifExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_operator('?', '?') + signode += addnodes.desc_sig_space() + self.thenExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_operator(':', ':') + signode += addnodes.desc_sig_space() + self.elseExpr.describe_signature(signode, mode, env, symbol) + + +class ASTBracedInitList(ASTBase): + def __init__(self, exprs: list[ASTExpression | ASTBracedInitList], + trailingComma: bool) -> None: + self.exprs = exprs + self.trailingComma = trailingComma + + def get_id(self, version: int) -> str: + return "il%sE" % ''.join(e.get_id(version) for e in self.exprs) + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = ', '.join(transform(e) for e in self.exprs) + trailingComma = ',' if self.trailingComma else '' + return f'{{{exprs}{trailingComma}}}' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('{', '{') + first = True + for e in self.exprs: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + else: + first = False + e.describe_signature(signode, mode, env, symbol) + if self.trailingComma: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_punctuation('}', '}') + + +class ASTAssignmentExpr(ASTExpression): + def __init__(self, leftExpr: ASTExpression, op: str, + rightExpr: ASTExpression | ASTBracedInitList): + self.leftExpr = leftExpr + self.op = op + self.rightExpr = rightExpr + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.leftExpr)) + res.append(' ') + res.append(self.op) + res.append(' ') + res.append(transform(self.rightExpr)) + return ''.join(res) + + def get_id(self, version: int) -> str: + # we end up generating the ID from left to right, instead of right to left + res = [] + res.append(_id_operator_v2[self.op]) + res.append(self.leftExpr.get_id(version)) + res.append(self.rightExpr.get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.leftExpr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + if ord(self.op[0]) >= ord('a') and ord(self.op[0]) <= ord('z'): + signode += addnodes.desc_sig_keyword(self.op, self.op) + else: + signode += addnodes.desc_sig_operator(self.op, self.op) + signode += addnodes.desc_sig_space() + self.rightExpr.describe_signature(signode, mode, env, symbol) + + +class ASTCommaExpr(ASTExpression): + def __init__(self, exprs: list[ASTExpression]): + assert len(exprs) > 0 + self.exprs = exprs + + def _stringify(self, transform: StringifyTransform) -> str: + return ', '.join(transform(e) for e in self.exprs) + + def get_id(self, version: int) -> str: + id_ = _id_operator_v2[','] + res = [] + for i in range(len(self.exprs) - 1): + res.append(id_) + res.append(self.exprs[i].get_id(version)) + res.append(self.exprs[-1].get_id(version)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.exprs[0].describe_signature(signode, mode, env, symbol) + for i in range(1, len(self.exprs)): + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + self.exprs[i].describe_signature(signode, mode, env, symbol) + + +class ASTFallbackExpr(ASTExpression): + def __init__(self, expr: str): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return self.expr + + def get_id(self, version: int) -> str: + return str(self.expr) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += nodes.literal(self.expr, self.expr) + + +################################################################################ +# Types +################################################################################ + +# Things for ASTNestedName +################################################################################ + +class ASTOperator(ASTBase): + def is_anon(self) -> bool: + return False + + def is_operator(self) -> bool: + return True + + def get_id(self, version: int) -> str: + raise NotImplementedError + + def _describe_identifier(self, signode: TextElement, identnode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + """Render the prefix into signode, and the last part into identnode.""" + raise NotImplementedError + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, prefix: str, templateArgs: str, + symbol: Symbol) -> None: + verify_description_mode(mode) + if mode == 'lastIsName': + mainName = addnodes.desc_name() + self._describe_identifier(mainName, mainName, env, symbol) + signode += mainName + elif mode == 'markType': + targetText = prefix + str(self) + templateArgs + pnode = addnodes.pending_xref('', refdomain='cpp', + reftype='identifier', + reftarget=targetText, modname=None, + classname=None) + pnode['cpp:parent_key'] = symbol.get_lookup_key() + # Render the identifier part, but collapse it into a string + # and make that the a link to this operator. + # E.g., if it is 'operator SomeType', then 'SomeType' becomes + # a link to the operator, not to 'SomeType'. + container = nodes.literal() + self._describe_identifier(signode, container, env, symbol) + txt = container.astext() + pnode += addnodes.desc_name(txt, txt) + signode += pnode + else: + addName = addnodes.desc_addname() + self._describe_identifier(addName, addName, env, symbol) + signode += addName + + +class ASTOperatorBuildIn(ASTOperator): + def __init__(self, op: str) -> None: + self.op = op + + def get_id(self, version: int) -> str: + if version == 1: + ids = _id_operator_v1 + if self.op not in ids: + raise NoOldIdError + else: + ids = _id_operator_v2 + if self.op not in ids: + raise Exception('Internal error: Built-in operator "%s" can not ' + 'be mapped to an id.' % self.op) + return ids[self.op] + + def _stringify(self, transform: StringifyTransform) -> str: + if self.op in ('new', 'new[]', 'delete', 'delete[]') or self.op[0] in "abcnox": + return 'operator ' + self.op + else: + return 'operator' + self.op + + def _describe_identifier(self, signode: TextElement, identnode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('operator', 'operator') + if self.op in ('new', 'new[]', 'delete', 'delete[]') or self.op[0] in "abcnox": + signode += addnodes.desc_sig_space() + identnode += addnodes.desc_sig_operator(self.op, self.op) + + +class ASTOperatorLiteral(ASTOperator): + def __init__(self, identifier: ASTIdentifier) -> None: + self.identifier = identifier + + def get_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError + return 'li' + self.identifier.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return 'operator""' + transform(self.identifier) + + def _describe_identifier(self, signode: TextElement, identnode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('operator', 'operator') + signode += addnodes.desc_sig_literal_string('""', '""') + self.identifier.describe_signature(identnode, 'markType', env, '', '', symbol) + + +class ASTOperatorType(ASTOperator): + def __init__(self, type: ASTType) -> None: + self.type = type + + def get_id(self, version: int) -> str: + if version == 1: + return 'castto-%s-operator' % self.type.get_id(version) + else: + return 'cv' + self.type.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return ''.join(['operator ', transform(self.type)]) + + def get_name_no_template(self) -> str: + return str(self) + + def _describe_identifier(self, signode: TextElement, identnode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('operator', 'operator') + signode += addnodes.desc_sig_space() + self.type.describe_signature(identnode, 'markType', env, symbol) + + +class ASTTemplateArgConstant(ASTBase): + def __init__(self, value: ASTExpression) -> None: + self.value = value + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.value) + + def get_id(self, version: int) -> str: + if version == 1: + return str(self).replace(' ', '-') + if version == 2: + return 'X' + str(self) + 'E' + return 'X' + self.value.get_id(version) + 'E' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.value.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateArgs(ASTBase): + def __init__(self, args: list[ASTType | ASTTemplateArgConstant], + packExpansion: bool) -> None: + assert args is not None + self.args = args + self.packExpansion = packExpansion + + def get_id(self, version: int) -> str: + if version == 1: + res = [] + res.append(':') + res.append('.'.join(a.get_id(version) for a in self.args)) + res.append(':') + return ''.join(res) + + res = [] + res.append('I') + if len(self.args) > 0: + for a in self.args[:-1]: + res.append(a.get_id(version)) + if self.packExpansion: + res.append('J') + res.append(self.args[-1].get_id(version)) + if self.packExpansion: + res.append('E') + res.append('E') + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = ', '.join(transform(a) for a in self.args) + if self.packExpansion: + res += '...' + return '<' + res + '>' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('<', '<') + first = True + for a in self.args: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + first = False + a.describe_signature(signode, 'markType', env, symbol=symbol) + if self.packExpansion: + signode += addnodes.desc_sig_punctuation('...', '...') + signode += addnodes.desc_sig_punctuation('>', '>') + + +# Main part of declarations +################################################################################ + +class ASTTrailingTypeSpec(ASTBase): + def get_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + +class ASTTrailingTypeSpecFundamental(ASTTrailingTypeSpec): + def __init__(self, names: list[str], canonNames: list[str]) -> None: + assert len(names) != 0 + assert len(names) == len(canonNames), (names, canonNames) + self.names = names + # the canonical name list is for ID lookup + self.canonNames = canonNames + + def _stringify(self, transform: StringifyTransform) -> str: + return ' '.join(self.names) + + def get_id(self, version: int) -> str: + if version == 1: + res = [] + for a in self.canonNames: + if a in _id_fundamental_v1: + res.append(_id_fundamental_v1[a]) + else: + res.append(a) + return '-'.join(res) + + txt = ' '.join(self.canonNames) + if txt not in _id_fundamental_v2: + raise Exception( + 'Semi-internal error: Fundamental type "%s" can not be mapped ' + 'to an ID. Is it a true fundamental type? If not so, the ' + 'parser should have rejected it.' % txt) + return _id_fundamental_v2[txt] + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + first = True + for n in self.names: + if not first: + signode += addnodes.desc_sig_space() + else: + first = False + signode += addnodes.desc_sig_keyword_type(n, n) + + +class ASTTrailingTypeSpecDecltypeAuto(ASTTrailingTypeSpec): + def _stringify(self, transform: StringifyTransform) -> str: + return 'decltype(auto)' + + def get_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError + return 'Dc' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('decltype', 'decltype') + signode += addnodes.desc_sig_punctuation('(', '(') + signode += addnodes.desc_sig_keyword('auto', 'auto') + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTTrailingTypeSpecDecltype(ASTTrailingTypeSpec): + def __init__(self, expr: ASTExpression): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return 'decltype(' + transform(self.expr) + ')' + + def get_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError + return 'DT' + self.expr.get_id(version) + "E" + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('decltype', 'decltype') + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTTrailingTypeSpecName(ASTTrailingTypeSpec): + def __init__(self, prefix: str, nestedName: ASTNestedName, + placeholderType: str | None) -> None: + self.prefix = prefix + self.nestedName = nestedName + self.placeholderType = placeholderType + + @property + def name(self) -> ASTNestedName: + return self.nestedName + + def get_id(self, version: int) -> str: + return self.nestedName.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.prefix: + res.append(self.prefix) + res.append(' ') + res.append(transform(self.nestedName)) + if self.placeholderType is not None: + res.append(' ') + res.append(self.placeholderType) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.prefix: + signode += addnodes.desc_sig_keyword(self.prefix, self.prefix) + signode += addnodes.desc_sig_space() + self.nestedName.describe_signature(signode, mode, env, symbol=symbol) + if self.placeholderType is not None: + signode += addnodes.desc_sig_space() + if self.placeholderType == 'auto': + signode += addnodes.desc_sig_keyword('auto', 'auto') + elif self.placeholderType == 'decltype(auto)': + signode += addnodes.desc_sig_keyword('decltype', 'decltype') + signode += addnodes.desc_sig_punctuation('(', '(') + signode += addnodes.desc_sig_keyword('auto', 'auto') + signode += addnodes.desc_sig_punctuation(')', ')') + else: + raise AssertionError(self.placeholderType) + + +class ASTFunctionParameter(ASTBase): + def __init__(self, arg: ASTTypeWithInit | ASTTemplateParamConstrainedTypeWithInit, + ellipsis: bool = False) -> None: + self.arg = arg + self.ellipsis = ellipsis + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + # else, do the usual + if self.ellipsis: + return 'z' + else: + return self.arg.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + if self.ellipsis: + return '...' + else: + return transform(self.arg) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.ellipsis: + signode += addnodes.desc_sig_punctuation('...', '...') + else: + self.arg.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTNoexceptSpec(ASTBase): + def __init__(self, expr: ASTExpression | None): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + if self.expr: + return 'noexcept(' + transform(self.expr) + ')' + return 'noexcept' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('noexcept', 'noexcept') + if self.expr: + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTParametersQualifiers(ASTBase): + def __init__(self, args: list[ASTFunctionParameter], volatile: bool, const: bool, + refQual: str | None, exceptionSpec: ASTNoexceptSpec, + trailingReturn: ASTType, + override: bool, final: bool, attrs: ASTAttributeList, + initializer: str | None) -> None: + self.args = args + self.volatile = volatile + self.const = const + self.refQual = refQual + self.exceptionSpec = exceptionSpec + self.trailingReturn = trailingReturn + self.override = override + self.final = final + self.attrs = attrs + self.initializer = initializer + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.args + + def get_modifiers_id(self, version: int) -> str: + res = [] + if self.volatile: + res.append('V') + if self.const: + if version == 1: + res.append('C') + else: + res.append('K') + if self.refQual == '&&': + res.append('O') + elif self.refQual == '&': + res.append('R') + return ''.join(res) + + def get_param_id(self, version: int) -> str: + if version == 1: + if len(self.args) == 0: + return '' + else: + return '__' + '.'.join(a.get_id(version) for a in self.args) + if len(self.args) == 0: + return 'v' + else: + return ''.join(a.get_id(version) for a in self.args) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append('(') + first = True + for a in self.args: + if not first: + res.append(', ') + first = False + res.append(str(a)) + res.append(')') + if self.volatile: + res.append(' volatile') + if self.const: + res.append(' const') + if self.refQual: + res.append(' ') + res.append(self.refQual) + if self.exceptionSpec: + res.append(' ') + res.append(transform(self.exceptionSpec)) + if self.trailingReturn: + res.append(' -> ') + res.append(transform(self.trailingReturn)) + if self.final: + res.append(' final') + if self.override: + res.append(' override') + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.attrs)) + if self.initializer: + res.append(' = ') + res.append(self.initializer) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + multi_line_parameter_list = False + test_node: Element = signode + while test_node.parent: + if not isinstance(test_node, addnodes.desc_signature): + test_node = test_node.parent + continue + multi_line_parameter_list = test_node.get('multi_line_parameter_list', False) + break + + # only use the desc_parameterlist for the outer list, not for inner lists + if mode == 'lastIsName': + paramlist = addnodes.desc_parameterlist() + paramlist['multi_line_parameter_list'] = multi_line_parameter_list + for arg in self.args: + param = addnodes.desc_parameter('', '', noemph=True) + arg.describe_signature(param, 'param', env, symbol=symbol) + paramlist += param + signode += paramlist + else: + signode += addnodes.desc_sig_punctuation('(', '(') + first = True + for arg in self.args: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + first = False + arg.describe_signature(signode, 'markType', env, symbol=symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + def _add_anno(signode: TextElement, text: str) -> None: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_keyword(text, text) + + if self.volatile: + _add_anno(signode, 'volatile') + if self.const: + _add_anno(signode, 'const') + if self.refQual: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation(self.refQual, self.refQual) + if self.exceptionSpec: + signode += addnodes.desc_sig_space() + self.exceptionSpec.describe_signature(signode, mode, env, symbol) + if self.trailingReturn: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_operator('->', '->') + signode += addnodes.desc_sig_space() + self.trailingReturn.describe_signature(signode, mode, env, symbol) + if self.final: + _add_anno(signode, 'final') + if self.override: + _add_anno(signode, 'override') + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.attrs.describe_signature(signode) + if self.initializer: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + assert self.initializer in ('0', 'delete', 'default') + if self.initializer == '0': + signode += addnodes.desc_sig_literal_number('0', '0') + else: + signode += addnodes.desc_sig_keyword(self.initializer, self.initializer) + + +class ASTExplicitSpec(ASTBase): + def __init__(self, expr: ASTExpression | None) -> None: + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['explicit'] + if self.expr is not None: + res.append('(') + res.append(transform(self.expr)) + res.append(')') + return ''.join(res) + + def describe_signature(self, signode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('explicit', 'explicit') + if self.expr is not None: + signode += addnodes.desc_sig_punctuation('(', '(') + self.expr.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTDeclSpecsSimple(ASTBase): + def __init__(self, storage: str, threadLocal: bool, inline: bool, virtual: bool, + explicitSpec: ASTExplicitSpec | None, + consteval: bool, constexpr: bool, constinit: bool, + volatile: bool, const: bool, friend: bool, + attrs: ASTAttributeList) -> None: + self.storage = storage + self.threadLocal = threadLocal + self.inline = inline + self.virtual = virtual + self.explicitSpec = explicitSpec + self.consteval = consteval + self.constexpr = constexpr + self.constinit = constinit + self.volatile = volatile + self.const = const + self.friend = friend + self.attrs = attrs + + def mergeWith(self, other: ASTDeclSpecsSimple) -> ASTDeclSpecsSimple: + if not other: + return self + return ASTDeclSpecsSimple(self.storage or other.storage, + self.threadLocal or other.threadLocal, + self.inline or other.inline, + self.virtual or other.virtual, + self.explicitSpec or other.explicitSpec, + self.consteval or other.consteval, + self.constexpr or other.constexpr, + self.constinit or other.constinit, + self.volatile or other.volatile, + self.const or other.const, + self.friend or other.friend, + self.attrs + other.attrs) + + def _stringify(self, transform: StringifyTransform) -> str: + res: list[str] = [] + if len(self.attrs) != 0: + res.append(transform(self.attrs)) + if self.storage: + res.append(self.storage) + if self.threadLocal: + res.append('thread_local') + if self.inline: + res.append('inline') + if self.friend: + res.append('friend') + if self.virtual: + res.append('virtual') + if self.explicitSpec: + res.append(transform(self.explicitSpec)) + if self.consteval: + res.append('consteval') + if self.constexpr: + res.append('constexpr') + if self.constinit: + res.append('constinit') + if self.volatile: + res.append('volatile') + if self.const: + res.append('const') + return ' '.join(res) + + def describe_signature(self, signode: TextElement, + env: BuildEnvironment, symbol: Symbol) -> None: + self.attrs.describe_signature(signode) + addSpace = len(self.attrs) != 0 + + def _add(signode: TextElement, text: str) -> bool: + if addSpace: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_keyword(text, text) + return True + + if self.storage: + addSpace = _add(signode, self.storage) + if self.threadLocal: + addSpace = _add(signode, 'thread_local') + if self.inline: + addSpace = _add(signode, 'inline') + if self.friend: + addSpace = _add(signode, 'friend') + if self.virtual: + addSpace = _add(signode, 'virtual') + if self.explicitSpec: + if addSpace: + signode += addnodes.desc_sig_space() + self.explicitSpec.describe_signature(signode, env, symbol) + addSpace = True + if self.consteval: + addSpace = _add(signode, 'consteval') + if self.constexpr: + addSpace = _add(signode, 'constexpr') + if self.constinit: + addSpace = _add(signode, 'constinit') + if self.volatile: + addSpace = _add(signode, 'volatile') + if self.const: + addSpace = _add(signode, 'const') + + +class ASTDeclSpecs(ASTBase): + def __init__(self, outer: str, + leftSpecs: ASTDeclSpecsSimple, rightSpecs: ASTDeclSpecsSimple, + trailing: ASTTrailingTypeSpec) -> None: + # leftSpecs and rightSpecs are used for output + # allSpecs are used for id generation + self.outer = outer + self.leftSpecs = leftSpecs + self.rightSpecs = rightSpecs + self.allSpecs = self.leftSpecs.mergeWith(self.rightSpecs) + self.trailingTypeSpec = trailing + + def get_id(self, version: int) -> str: + if version == 1: + res = [] + res.append(self.trailingTypeSpec.get_id(version)) + if self.allSpecs.volatile: + res.append('V') + if self.allSpecs.const: + res.append('C') + return ''.join(res) + res = [] + if self.allSpecs.volatile: + res.append('V') + if self.allSpecs.const: + res.append('K') + if self.trailingTypeSpec is not None: + res.append(self.trailingTypeSpec.get_id(version)) + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res: list[str] = [] + l = transform(self.leftSpecs) + if len(l) > 0: + res.append(l) + if self.trailingTypeSpec: + if len(res) > 0: + res.append(" ") + res.append(transform(self.trailingTypeSpec)) + r = str(self.rightSpecs) + if len(r) > 0: + if len(res) > 0: + res.append(" ") + res.append(r) + return "".join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + numChildren = len(signode) + self.leftSpecs.describe_signature(signode, env, symbol) + addSpace = len(signode) != numChildren + + if self.trailingTypeSpec: + if addSpace: + signode += addnodes.desc_sig_space() + numChildren = len(signode) + self.trailingTypeSpec.describe_signature(signode, mode, env, + symbol=symbol) + addSpace = len(signode) != numChildren + + if len(str(self.rightSpecs)) > 0: + if addSpace: + signode += addnodes.desc_sig_space() + self.rightSpecs.describe_signature(signode, env, symbol) + + +# Declarator +################################################################################ + +class ASTArray(ASTBase): + def __init__(self, size: ASTExpression): + self.size = size + + def _stringify(self, transform: StringifyTransform) -> str: + if self.size: + return '[' + transform(self.size) + ']' + else: + return '[]' + + def get_id(self, version: int) -> str: + if version == 1: + return 'A' + if version == 2: + if self.size: + return 'A' + str(self.size) + '_' + else: + return 'A_' + if self.size: + return 'A' + self.size.get_id(version) + '_' + else: + return 'A_' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('[', '[') + if self.size: + self.size.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation(']', ']') + + +class ASTDeclarator(ASTBase): + @property + def name(self) -> ASTNestedName: + raise NotImplementedError(repr(self)) + + @name.setter + def name(self, name: ASTNestedName) -> None: + raise NotImplementedError(repr(self)) + + @property + def isPack(self) -> bool: + raise NotImplementedError(repr(self)) + + @property + def function_params(self) -> list[ASTFunctionParameter]: + raise NotImplementedError(repr(self)) + + @property + def trailingReturn(self) -> ASTType: + raise NotImplementedError(repr(self)) + + def require_space_after_declSpecs(self) -> bool: + raise NotImplementedError(repr(self)) + + def get_modifiers_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_param_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_ptr_suffix_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + raise NotImplementedError(repr(self)) + + def is_function_type(self) -> bool: + raise NotImplementedError(repr(self)) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + +class ASTDeclaratorNameParamQual(ASTDeclarator): + def __init__(self, declId: ASTNestedName, + arrayOps: list[ASTArray], + paramQual: ASTParametersQualifiers) -> None: + self.declId = declId + self.arrayOps = arrayOps + self.paramQual = paramQual + + @property + def name(self) -> ASTNestedName: + return self.declId + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.declId = name + + @property + def isPack(self) -> bool: + return False + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.paramQual.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.paramQual.trailingReturn + + # only the modifiers for a function, e.g., + def get_modifiers_id(self, version: int) -> str: + # cv-qualifiers + if self.paramQual: + return self.paramQual.get_modifiers_id(version) + raise Exception("This should only be called on a function: %s" % self) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + if self.paramQual: + return self.paramQual.get_param_id(version) + else: + return '' + + def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers + return ''.join(a.get_id(version) for a in self.arrayOps) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + res = [] + # TODO: can we actually have both array ops and paramQual? + res.append(self.get_ptr_suffix_id(version)) + if self.paramQual: + res.append(self.get_modifiers_id(version)) + res.append('F') + res.append(returnTypeId) + res.append(self.get_param_id(version)) + res.append('E') + else: + res.append(returnTypeId) + return ''.join(res) + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def is_function_type(self) -> bool: + return self.paramQual is not None + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + for op in self.arrayOps: + res.append(transform(op)) + if self.paramQual: + res.append(transform(self.paramQual)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + for op in self.arrayOps: + op.describe_signature(signode, mode, env, symbol) + if self.paramQual: + self.paramQual.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorNameBitField(ASTDeclarator): + def __init__(self, declId: ASTNestedName, size: ASTExpression): + self.declId = declId + self.size = size + + @property + def name(self) -> ASTNestedName: + return self.declId + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.declId = name + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + return '' + + def get_ptr_suffix_id(self, version: int) -> str: # only the array specifiers + return '' + + # ------------------------------------------------------------------------ + + def require_space_after_declSpecs(self) -> bool: + return self.declId is not None + + def is_function_type(self) -> bool: + return False + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.declId: + res.append(transform(self.declId)) + res.append(" : ") + res.append(transform(self.size)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.declId: + self.declId.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation(':', ':') + signode += addnodes.desc_sig_space() + self.size.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorPtr(ASTDeclarator): + def __init__(self, next: ASTDeclarator, volatile: bool, const: bool, + attrs: ASTAttributeList) -> None: + assert next + self.next = next + self.volatile = volatile + self.const = const + self.attrs = attrs + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + + @property + def isPack(self) -> bool: + return self.next.isPack + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.next.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.next.trailingReturn + + def require_space_after_declSpecs(self) -> bool: + return self.next.require_space_after_declSpecs() + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['*'] + res.append(transform(self.attrs)) + if len(self.attrs) != 0 and (self.volatile or self.const): + res.append(' ') + if self.volatile: + res.append('volatile') + if self.const: + if self.volatile: + res.append(' ') + res.append('const') + if self.const or self.volatile or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + res.append(' ') + res.append(transform(self.next)) + return ''.join(res) + + def get_modifiers_id(self, version: int) -> str: + return self.next.get_modifiers_id(version) + + def get_param_id(self, version: int) -> str: + return self.next.get_param_id(version) + + def get_ptr_suffix_id(self, version: int) -> str: + if version == 1: + res = ['P'] + if self.volatile: + res.append('V') + if self.const: + res.append('C') + res.append(self.next.get_ptr_suffix_id(version)) + return ''.join(res) + + res = [self.next.get_ptr_suffix_id(version)] + res.append('P') + if self.volatile: + res.append('V') + if self.const: + res.append('C') + return ''.join(res) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + # ReturnType *next, so we are part of the return type of 'next + res = ['P'] + if self.volatile: + res.append('V') + if self.const: + res.append('C') + res.append(returnTypeId) + return self.next.get_type_id(version, returnTypeId=''.join(res)) + + def is_function_type(self) -> bool: + return self.next.is_function_type() + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('*', '*') + self.attrs.describe_signature(signode) + if len(self.attrs) != 0 and (self.volatile or self.const): + signode += addnodes.desc_sig_space() + + def _add_anno(signode: TextElement, text: str) -> None: + signode += addnodes.desc_sig_keyword(text, text) + if self.volatile: + _add_anno(signode, 'volatile') + if self.const: + if self.volatile: + signode += addnodes.desc_sig_space() + _add_anno(signode, 'const') + if self.const or self.volatile or len(self.attrs) > 0: + if self.next.require_space_after_declSpecs(): + signode += addnodes.desc_sig_space() + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorRef(ASTDeclarator): + def __init__(self, next: ASTDeclarator, attrs: ASTAttributeList) -> None: + assert next + self.next = next + self.attrs = attrs + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + + @property + def isPack(self) -> bool: + return self.next.isPack + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.next.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.next.trailingReturn + + def require_space_after_declSpecs(self) -> bool: + return self.next.require_space_after_declSpecs() + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['&'] + res.append(transform(self.attrs)) + if len(self.attrs) != 0 and self.next.require_space_after_declSpecs(): + res.append(' ') + res.append(transform(self.next)) + return ''.join(res) + + def get_modifiers_id(self, version: int) -> str: + return self.next.get_modifiers_id(version) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + return self.next.get_param_id(version) + + def get_ptr_suffix_id(self, version: int) -> str: + if version == 1: + return 'R' + self.next.get_ptr_suffix_id(version) + else: + return self.next.get_ptr_suffix_id(version) + 'R' + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + # ReturnType &next, so we are part of the return type of 'next + return self.next.get_type_id(version, returnTypeId='R' + returnTypeId) + + def is_function_type(self) -> bool: + return self.next.is_function_type() + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('&', '&') + self.attrs.describe_signature(signode) + if len(self.attrs) > 0 and self.next.require_space_after_declSpecs(): + signode += addnodes.desc_sig_space() + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorParamPack(ASTDeclarator): + def __init__(self, next: ASTDeclarator) -> None: + assert next + self.next = next + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.next.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.next.trailingReturn + + @property + def isPack(self) -> bool: + return True + + def require_space_after_declSpecs(self) -> bool: + return False + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.next) + if self.next.name: + res = ' ' + res + return '...' + res + + def get_modifiers_id(self, version: int) -> str: + return self.next.get_modifiers_id(version) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + return self.next.get_param_id(version) + + def get_ptr_suffix_id(self, version: int) -> str: + if version == 1: + return 'Dp' + self.next.get_ptr_suffix_id(version) + else: + return self.next.get_ptr_suffix_id(version) + 'Dp' + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + # ReturnType... next, so we are part of the return type of 'next + return self.next.get_type_id(version, returnTypeId='Dp' + returnTypeId) + + def is_function_type(self) -> bool: + return self.next.is_function_type() + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('...', '...') + if self.next.name: + signode += addnodes.desc_sig_space() + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorMemPtr(ASTDeclarator): + def __init__(self, className: ASTNestedName, + const: bool, volatile: bool, next: ASTDeclarator) -> None: + assert className + assert next + self.className = className + self.const = const + self.volatile = volatile + self.next = next + + @property + def name(self) -> ASTNestedName: + return self.next.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + + @property + def isPack(self): + return self.next.isPack + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.next.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.next.trailingReturn + + def require_space_after_declSpecs(self) -> bool: + return True + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.className)) + res.append('::*') + if self.volatile: + res.append('volatile') + if self.const: + if self.volatile: + res.append(' ') + res.append('const') + if self.next.require_space_after_declSpecs(): + res.append(' ') + res.append(transform(self.next)) + return ''.join(res) + + def get_modifiers_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError + return self.next.get_modifiers_id(version) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + if version == 1: + raise NoOldIdError + return self.next.get_param_id(version) + + def get_ptr_suffix_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError + raise NotImplementedError + return self.next.get_ptr_suffix_id(version) + 'Dp' + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + # ReturnType name::* next, so we are part of the return type of next + nextReturnTypeId = '' + if self.volatile: + nextReturnTypeId += 'V' + if self.const: + nextReturnTypeId += 'K' + nextReturnTypeId += 'M' + nextReturnTypeId += self.className.get_id(version) + nextReturnTypeId += returnTypeId + return self.next.get_type_id(version, nextReturnTypeId) + + def is_function_type(self) -> bool: + return self.next.is_function_type() + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.className.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation('::', '::') + signode += addnodes.desc_sig_punctuation('*', '*') + + def _add_anno(signode: TextElement, text: str) -> None: + signode += addnodes.desc_sig_keyword(text, text) + if self.volatile: + _add_anno(signode, 'volatile') + if self.const: + if self.volatile: + signode += addnodes.desc_sig_space() + _add_anno(signode, 'const') + if self.next.require_space_after_declSpecs(): + signode += addnodes.desc_sig_space() + self.next.describe_signature(signode, mode, env, symbol) + + +class ASTDeclaratorParen(ASTDeclarator): + def __init__(self, inner: ASTDeclarator, next: ASTDeclarator) -> None: + assert inner + assert next + self.inner = inner + self.next = next + # TODO: we assume the name, params, and qualifiers are in inner + + @property + def name(self) -> ASTNestedName: + return self.inner.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.inner.name = name + + @property + def isPack(self): + return self.inner.isPack or self.next.isPack + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.inner.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.inner.trailingReturn + + def require_space_after_declSpecs(self) -> bool: + return True + + def _stringify(self, transform: StringifyTransform) -> str: + res = ['('] + res.append(transform(self.inner)) + res.append(')') + res.append(transform(self.next)) + return ''.join(res) + + def get_modifiers_id(self, version: int) -> str: + return self.inner.get_modifiers_id(version) + + def get_param_id(self, version: int) -> str: # only the parameters (if any) + return self.inner.get_param_id(version) + + def get_ptr_suffix_id(self, version: int) -> str: + if version == 1: + raise NoOldIdError # TODO: was this implemented before? + return self.next.get_ptr_suffix_id(version) + \ + self.inner.get_ptr_suffix_id(version) + return self.inner.get_ptr_suffix_id(version) + \ + self.next.get_ptr_suffix_id(version) + + def get_type_id(self, version: int, returnTypeId: str) -> str: + assert version >= 2 + # ReturnType (inner)next, so 'inner' returns everything outside + nextId = self.next.get_type_id(version, returnTypeId) + return self.inner.get_type_id(version, returnTypeId=nextId) + + def is_function_type(self) -> bool: + return self.inner.is_function_type() + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('(', '(') + self.inner.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + self.next.describe_signature(signode, "noneIsName", env, symbol) + + +# Type and initializer stuff +############################################################################################## + +class ASTPackExpansionExpr(ASTExpression): + def __init__(self, expr: ASTExpression | ASTBracedInitList): + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.expr) + '...' + + def get_id(self, version: int) -> str: + id = self.expr.get_id(version) + return 'sp' + id + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.expr.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation('...', '...') + + +class ASTParenExprList(ASTBaseParenExprList): + def __init__(self, exprs: list[ASTExpression | ASTBracedInitList]) -> None: + self.exprs = exprs + + def get_id(self, version: int) -> str: + return "pi%sE" % ''.join(e.get_id(version) for e in self.exprs) + + def _stringify(self, transform: StringifyTransform) -> str: + exprs = [transform(e) for e in self.exprs] + return '(%s)' % ', '.join(exprs) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + signode += addnodes.desc_sig_punctuation('(', '(') + first = True + for e in self.exprs: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + else: + first = False + e.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation(')', ')') + + +class ASTInitializer(ASTBase): + def __init__(self, value: ASTExpression | ASTBracedInitList, + hasAssign: bool = True) -> None: + self.value = value + self.hasAssign = hasAssign + + def _stringify(self, transform: StringifyTransform) -> str: + val = transform(self.value) + if self.hasAssign: + return ' = ' + val + else: + return val + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.hasAssign: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + self.value.describe_signature(signode, 'markType', env, symbol) + + +class ASTType(ASTBase): + def __init__(self, declSpecs: ASTDeclSpecs, decl: ASTDeclarator) -> None: + assert declSpecs + assert decl + self.declSpecs = declSpecs + self.decl = decl + + @property + def name(self) -> ASTNestedName: + return self.decl.name + + @name.setter + def name(self, name: ASTNestedName) -> None: + self.decl.name = name + + @property + def isPack(self) -> bool: + return self.decl.isPack + + @property + def function_params(self) -> list[ASTFunctionParameter]: + return self.decl.function_params + + @property + def trailingReturn(self) -> ASTType: + return self.decl.trailingReturn + + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: + if version == 1: + res = [] + if objectType: # needs the name + if objectType == 'function': # also modifiers + res.append(symbol.get_full_nested_name().get_id(version)) + res.append(self.decl.get_param_id(version)) + res.append(self.decl.get_modifiers_id(version)) + if (self.declSpecs.leftSpecs.constexpr or + (self.declSpecs.rightSpecs and + self.declSpecs.rightSpecs.constexpr)): + res.append('CE') + elif objectType == 'type': # just the name + res.append(symbol.get_full_nested_name().get_id(version)) + else: + raise AssertionError(objectType) + else: # only type encoding + if self.decl.is_function_type(): + raise NoOldIdError + res.append(self.declSpecs.get_id(version)) + res.append(self.decl.get_ptr_suffix_id(version)) + res.append(self.decl.get_param_id(version)) + return ''.join(res) + # other versions + res = [] + if objectType: # needs the name + if objectType == 'function': # also modifiers + modifiers = self.decl.get_modifiers_id(version) + res.append(symbol.get_full_nested_name().get_id(version, modifiers)) + if version >= 4: + # with templates we need to mangle the return type in as well + templ = symbol.declaration.templatePrefix + if templ is not None: + typeId = self.decl.get_ptr_suffix_id(version) + if self.trailingReturn: + returnTypeId = self.trailingReturn.get_id(version) + else: + returnTypeId = self.declSpecs.get_id(version) + res.append(typeId) + res.append(returnTypeId) + res.append(self.decl.get_param_id(version)) + elif objectType == 'type': # just the name + res.append(symbol.get_full_nested_name().get_id(version)) + else: + raise AssertionError(objectType) + else: # only type encoding + # the 'returnType' of a non-function type is simply just the last + # type, i.e., for 'int*' it is 'int' + returnTypeId = self.declSpecs.get_id(version) + typeId = self.decl.get_type_id(version, returnTypeId) + res.append(typeId) + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + declSpecs = transform(self.declSpecs) + res.append(declSpecs) + if self.decl.require_space_after_declSpecs() and len(declSpecs) > 0: + res.append(' ') + res.append(transform(self.decl)) + return ''.join(res) + + def get_type_declaration_prefix(self) -> str: + if self.declSpecs.trailingTypeSpec: + return 'typedef' + else: + return 'type' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.declSpecs.describe_signature(signode, 'markType', env, symbol) + if (self.decl.require_space_after_declSpecs() and + len(str(self.declSpecs)) > 0): + signode += addnodes.desc_sig_space() + # for parameters that don't really declare new names we get 'markType', + # this should not be propagated, but be 'noneIsName'. + if mode == 'markType': + mode = 'noneIsName' + self.decl.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParamConstrainedTypeWithInit(ASTBase): + def __init__(self, type: ASTType, init: ASTType) -> None: + assert type + self.type = type + self.init = init + + @property + def name(self) -> ASTNestedName: + return self.type.name + + @property + def isPack(self) -> bool: + return self.type.isPack + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + # this is not part of the normal name mangling in C++ + assert version >= 2 + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + else: + return self.type.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.type) + if self.init: + res += " = " + res += transform(self.init) + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.type.describe_signature(signode, mode, env, symbol) + if self.init: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + self.init.describe_signature(signode, mode, env, symbol) + + +class ASTTypeWithInit(ASTBase): + def __init__(self, type: ASTType, init: ASTInitializer) -> None: + self.type = type + self.init = init + + @property + def name(self) -> ASTNestedName: + return self.type.name + + @property + def isPack(self) -> bool: + return self.type.isPack + + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: + if objectType != 'member': + return self.type.get_id(version, objectType) + if version == 1: + return (symbol.get_full_nested_name().get_id(version) + '__' + + self.type.get_id(version)) + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.type)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.type.describe_signature(signode, mode, env, symbol) + if self.init: + self.init.describe_signature(signode, mode, env, symbol) + + +class ASTTypeUsing(ASTBase): + def __init__(self, name: ASTNestedName, type: ASTType) -> None: + self.name = name + self.type = type + + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: + if version == 1: + raise NoOldIdError + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.name)) + if self.type: + res.append(' = ') + res.append(transform(self.type)) + return ''.join(res) + + def get_type_declaration_prefix(self) -> str: + return 'using' + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol=symbol) + if self.type: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + self.type.describe_signature(signode, 'markType', env, symbol=symbol) + + +# Other declarations +############################################################################################## + +class ASTConcept(ASTBase): + def __init__(self, nestedName: ASTNestedName, initializer: ASTInitializer) -> None: + self.nestedName = nestedName + self.initializer = initializer + + @property + def name(self) -> ASTNestedName: + return self.nestedName + + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: + if version == 1: + raise NoOldIdError + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.nestedName) + if self.initializer: + res += transform(self.initializer) + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.nestedName.describe_signature(signode, mode, env, symbol) + if self.initializer: + self.initializer.describe_signature(signode, mode, env, symbol) + + +class ASTBaseClass(ASTBase): + def __init__(self, name: ASTNestedName, visibility: str, + virtual: bool, pack: bool) -> None: + self.name = name + self.visibility = visibility + self.virtual = virtual + self.pack = pack + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.visibility is not None: + res.append(self.visibility) + res.append(' ') + if self.virtual: + res.append('virtual ') + res.append(transform(self.name)) + if self.pack: + res.append('...') + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + if self.visibility is not None: + signode += addnodes.desc_sig_keyword(self.visibility, + self.visibility) + signode += addnodes.desc_sig_space() + if self.virtual: + signode += addnodes.desc_sig_keyword('virtual', 'virtual') + signode += addnodes.desc_sig_space() + self.name.describe_signature(signode, 'markType', env, symbol=symbol) + if self.pack: + signode += addnodes.desc_sig_punctuation('...', '...') + + +class ASTClass(ASTBase): + def __init__(self, name: ASTNestedName, final: bool, bases: list[ASTBaseClass], + attrs: ASTAttributeList) -> None: + self.name = name + self.final = final + self.bases = bases + self.attrs = attrs + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.attrs)) + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.name)) + if self.final: + res.append(' final') + if len(self.bases) > 0: + res.append(' : ') + first = True + for b in self.bases: + if not first: + res.append(', ') + first = False + res.append(transform(b)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.attrs.describe_signature(signode) + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.name.describe_signature(signode, mode, env, symbol=symbol) + if self.final: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_keyword('final', 'final') + if len(self.bases) > 0: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation(':', ':') + signode += addnodes.desc_sig_space() + for b in self.bases: + b.describe_signature(signode, mode, env, symbol=symbol) + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + signode.pop() + signode.pop() + + +class ASTUnion(ASTBase): + def __init__(self, name: ASTNestedName, attrs: ASTAttributeList) -> None: + self.name = name + self.attrs = attrs + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + if version == 1: + raise NoOldIdError + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.attrs)) + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.name)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.attrs.describe_signature(signode) + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.name.describe_signature(signode, mode, env, symbol=symbol) + + +class ASTEnum(ASTBase): + def __init__(self, name: ASTNestedName, scoped: str, underlyingType: ASTType, + attrs: ASTAttributeList) -> None: + self.name = name + self.scoped = scoped + self.underlyingType = underlyingType + self.attrs = attrs + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + if version == 1: + raise NoOldIdError + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.scoped: + res.append(self.scoped) + res.append(' ') + res.append(transform(self.attrs)) + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.name)) + if self.underlyingType: + res.append(' : ') + res.append(transform(self.underlyingType)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + # self.scoped has been done by the CPPEnumObject + self.attrs.describe_signature(signode) + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.name.describe_signature(signode, mode, env, symbol=symbol) + if self.underlyingType: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation(':', ':') + signode += addnodes.desc_sig_space() + self.underlyingType.describe_signature(signode, 'noneIsName', + env, symbol=symbol) + + +class ASTEnumerator(ASTBase): + def __init__(self, name: ASTNestedName, init: ASTInitializer | None, + attrs: ASTAttributeList) -> None: + self.name = name + self.init = init + self.attrs = attrs + + def get_id(self, version: int, objectType: str, symbol: Symbol) -> str: + if version == 1: + raise NoOldIdError + return symbol.get_full_nested_name().get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.name)) + if len(self.attrs) != 0: + res.append(' ') + res.append(transform(self.attrs)) + if self.init: + res.append(transform(self.init)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + verify_description_mode(mode) + self.name.describe_signature(signode, mode, env, symbol) + if len(self.attrs) != 0: + signode += addnodes.desc_sig_space() + self.attrs.describe_signature(signode) + if self.init: + self.init.describe_signature(signode, 'markType', env, symbol) + + +################################################################################ +# Templates +################################################################################ + +# Parameters +################################################################################ + +class ASTTemplateParam(ASTBase): + def get_identifier(self) -> ASTIdentifier: + raise NotImplementedError(repr(self)) + + def get_id(self, version: int) -> str: + raise NotImplementedError(repr(self)) + + def describe_signature(self, parentNode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + raise NotImplementedError(repr(self)) + + @property + def isPack(self) -> bool: + raise NotImplementedError(repr(self)) + + @property + def name(self) -> ASTNestedName: + raise NotImplementedError(repr(self)) + + +class ASTTemplateKeyParamPackIdDefault(ASTTemplateParam): + def __init__(self, key: str, identifier: ASTIdentifier, + parameterPack: bool, default: ASTType) -> None: + assert key + if parameterPack: + assert default is None + self.key = key + self.identifier = identifier + self.parameterPack = parameterPack + self.default = default + + def get_identifier(self) -> ASTIdentifier: + return self.identifier + + def get_id(self, version: int) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + res = [] + if self.parameterPack: + res.append('Dp') + else: + res.append('0') # we need to put something + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [self.key] + if self.parameterPack: + if self.identifier: + res.append(' ') + res.append('...') + if self.identifier: + if not self.parameterPack: + res.append(' ') + res.append(transform(self.identifier)) + if self.default: + res.append(' = ') + res.append(transform(self.default)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword(self.key, self.key) + if self.parameterPack: + if self.identifier: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('...', '...') + if self.identifier: + if not self.parameterPack: + signode += addnodes.desc_sig_space() + self.identifier.describe_signature(signode, mode, env, '', '', symbol) + if self.default: + signode += addnodes.desc_sig_space() + signode += addnodes.desc_sig_punctuation('=', '=') + signode += addnodes.desc_sig_space() + self.default.describe_signature(signode, 'markType', env, symbol) + + +class ASTTemplateParamType(ASTTemplateParam): + def __init__(self, data: ASTTemplateKeyParamPackIdDefault) -> None: + assert data + self.data = data + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.data.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.data.get_identifier() + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + # this is not part of the normal name mangling in C++ + assert version >= 2 + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=False) + else: + return self.data.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.data) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.data.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParamTemplateType(ASTTemplateParam): + def __init__(self, nestedParams: ASTTemplateParams, + data: ASTTemplateKeyParamPackIdDefault) -> None: + assert nestedParams + assert data + self.nestedParams = nestedParams + self.data = data + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.data.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.data.get_identifier() + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + return self.nestedParams.get_id(version) + self.data.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + return transform(self.nestedParams) + transform(self.data) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.nestedParams.describe_signature(signode, 'noneIsName', env, symbol) + signode += addnodes.desc_sig_space() + self.data.describe_signature(signode, mode, env, symbol) + + +class ASTTemplateParamNonType(ASTTemplateParam): + def __init__(self, + param: ASTTypeWithInit | ASTTemplateParamConstrainedTypeWithInit, + parameterPack: bool = False) -> None: + assert param + self.param = param + self.parameterPack = parameterPack + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.param.isPack or self.parameterPack + + def get_identifier(self) -> ASTIdentifier: + name = self.param.name + if name: + assert len(name.names) == 1 + assert name.names[0].identOrOp + assert not name.names[0].templateArgs + res = name.names[0].identOrOp + assert isinstance(res, ASTIdentifier) + return res + else: + return None + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + res = '_' + if self.parameterPack: + res += 'Dp' + return res + self.param.get_id(version) + + def _stringify(self, transform: StringifyTransform) -> str: + res = transform(self.param) + if self.parameterPack: + res += '...' + return res + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + self.param.describe_signature(signode, mode, env, symbol) + if self.parameterPack: + signode += addnodes.desc_sig_punctuation('...', '...') + + +class ASTTemplateParams(ASTBase): + def __init__(self, params: list[ASTTemplateParam], + requiresClause: ASTRequiresClause | None) -> None: + assert params is not None + self.params = params + self.requiresClause = requiresClause + + def get_id(self, version: int, excludeRequires: bool = False) -> str: + assert version >= 2 + res = [] + res.append("I") + for param in self.params: + res.append(param.get_id(version)) + res.append("E") + if not excludeRequires and self.requiresClause: + res.append('IQ') + res.append(self.requiresClause.expr.get_id(version)) + res.append('E') + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append("template<") + res.append(", ".join(transform(a) for a in self.params)) + res.append("> ") + if self.requiresClause is not None: + res.append(transform(self.requiresClause)) + res.append(" ") + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('template', 'template') + signode += addnodes.desc_sig_punctuation('<', '<') + first = True + for param in self.params: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + first = False + param.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation('>', '>') + if self.requiresClause is not None: + signode += addnodes.desc_sig_space() + self.requiresClause.describe_signature(signode, mode, env, symbol) + + def describe_signature_as_introducer( + self, parentNode: desc_signature, mode: str, env: BuildEnvironment, + symbol: Symbol, lineSpec: bool) -> None: + def makeLine(parentNode: desc_signature) -> addnodes.desc_signature_line: + signode = addnodes.desc_signature_line() + parentNode += signode + signode.sphinx_line_type = 'templateParams' + return signode + lineNode = makeLine(parentNode) + lineNode += addnodes.desc_sig_keyword('template', 'template') + lineNode += addnodes.desc_sig_punctuation('<', '<') + first = True + for param in self.params: + if not first: + lineNode += addnodes.desc_sig_punctuation(',', ',') + lineNode += addnodes.desc_sig_space() + first = False + if lineSpec: + lineNode = makeLine(parentNode) + param.describe_signature(lineNode, mode, env, symbol) + if lineSpec and not first: + lineNode = makeLine(parentNode) + lineNode += addnodes.desc_sig_punctuation('>', '>') + if self.requiresClause: + reqNode = addnodes.desc_signature_line() + reqNode.sphinx_line_type = 'requiresClause' + parentNode += reqNode + self.requiresClause.describe_signature(reqNode, 'markType', env, symbol) + + +# Template introducers +################################################################################ + +class ASTTemplateIntroductionParameter(ASTBase): + def __init__(self, identifier: ASTIdentifier, parameterPack: bool) -> None: + self.identifier = identifier + self.parameterPack = parameterPack + + @property + def name(self) -> ASTNestedName: + id = self.get_identifier() + return ASTNestedName([ASTNestedNameElement(id, None)], [False], rooted=False) + + @property + def isPack(self) -> bool: + return self.parameterPack + + def get_identifier(self) -> ASTIdentifier: + return self.identifier + + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: + assert version >= 2 + # this is not part of the normal name mangling in C++ + if symbol: + # the anchor will be our parent + return symbol.parent.declaration.get_id(version, prefixed=None) + else: + if self.parameterPack: + return 'Dp' + else: + return '0' # we need to put something + + def get_id_as_arg(self, version: int) -> str: + assert version >= 2 + # used for the implicit requires clause + res = self.identifier.get_id(version) + if self.parameterPack: + return 'sp' + res + else: + return res + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.parameterPack: + res.append('...') + res.append(transform(self.identifier)) + return ''.join(res) + + def describe_signature(self, signode: TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + if self.parameterPack: + signode += addnodes.desc_sig_punctuation('...', '...') + self.identifier.describe_signature(signode, mode, env, '', '', symbol) + + +class ASTTemplateIntroduction(ASTBase): + def __init__(self, concept: ASTNestedName, + params: list[ASTTemplateIntroductionParameter]) -> None: + assert len(params) > 0 + self.concept = concept + self.params = params + + def get_id(self, version: int) -> str: + assert version >= 2 + # first do the same as a normal template parameter list + res = [] + res.append("I") + for param in self.params: + res.append(param.get_id(version)) + res.append("E") + # let's use X expr E, which is otherwise for constant template args + res.append("X") + res.append(self.concept.get_id(version)) + res.append("I") + for param in self.params: + res.append(param.get_id_as_arg(version)) + res.append("E") + res.append("E") + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + res.append(transform(self.concept)) + res.append('{') + res.append(', '.join(transform(param) for param in self.params)) + res.append('} ') + return ''.join(res) + + def describe_signature_as_introducer( + self, parentNode: desc_signature, mode: str, + env: BuildEnvironment, symbol: Symbol, lineSpec: bool) -> None: + # Note: 'lineSpec' has no effect on template introductions. + signode = addnodes.desc_signature_line() + parentNode += signode + signode.sphinx_line_type = 'templateIntroduction' + self.concept.describe_signature(signode, 'markType', env, symbol) + signode += addnodes.desc_sig_punctuation('{', '{') + first = True + for param in self.params: + if not first: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + first = False + param.describe_signature(signode, mode, env, symbol) + signode += addnodes.desc_sig_punctuation('}', '}') + + +################################################################################ + +class ASTTemplateDeclarationPrefix(ASTBase): + def __init__(self, + templates: list[ASTTemplateParams | ASTTemplateIntroduction]) -> None: + # templates is None means it's an explicit instantiation of a variable + self.templates = templates + + def get_requires_clause_in_last(self) -> ASTRequiresClause | None: + if self.templates is None: + return None + lastList = self.templates[-1] + if not isinstance(lastList, ASTTemplateParams): + return None + return lastList.requiresClause # which may be None + + def get_id_except_requires_clause_in_last(self, version: int) -> str: + assert version >= 2 + # This is not part of the Itanium ABI mangling system. + res = [] + lastIndex = len(self.templates) - 1 + for i, t in enumerate(self.templates): + if isinstance(t, ASTTemplateParams): + res.append(t.get_id(version, excludeRequires=(i == lastIndex))) + else: + res.append(t.get_id(version)) + return ''.join(res) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + for t in self.templates: + res.append(transform(t)) + return ''.join(res) + + def describe_signature(self, signode: desc_signature, mode: str, + env: BuildEnvironment, symbol: Symbol, lineSpec: bool) -> None: + verify_description_mode(mode) + for t in self.templates: + t.describe_signature_as_introducer(signode, 'lastIsName', env, symbol, lineSpec) + + +class ASTRequiresClause(ASTBase): + def __init__(self, expr: ASTExpression) -> None: + self.expr = expr + + def _stringify(self, transform: StringifyTransform) -> str: + return 'requires ' + transform(self.expr) + + def describe_signature(self, signode: nodes.TextElement, mode: str, + env: BuildEnvironment, symbol: Symbol) -> None: + signode += addnodes.desc_sig_keyword('requires', 'requires') + signode += addnodes.desc_sig_space() + self.expr.describe_signature(signode, mode, env, symbol) + + +################################################################################ +################################################################################ + +class ASTDeclaration(ASTBase): + def __init__(self, objectType: str, directiveType: str | None = None, + visibility: str | None = None, + templatePrefix: ASTTemplateDeclarationPrefix | None = None, + declaration: Any = None, + trailingRequiresClause: ASTRequiresClause | None = None, + semicolon: bool = False) -> None: + self.objectType = objectType + self.directiveType = directiveType + self.visibility = visibility + self.templatePrefix = templatePrefix + self.declaration = declaration + self.trailingRequiresClause = trailingRequiresClause + self.semicolon = semicolon + + self.symbol: Symbol = None + # set by CPPObject._add_enumerator_to_parent + self.enumeratorScopedSymbol: Symbol = None + + def clone(self) -> ASTDeclaration: + templatePrefixClone = self.templatePrefix.clone() if self.templatePrefix else None + trailingRequiresClasueClone = self.trailingRequiresClause.clone() \ + if self.trailingRequiresClause else None + return ASTDeclaration(self.objectType, self.directiveType, self.visibility, + templatePrefixClone, + self.declaration.clone(), trailingRequiresClasueClone, + self.semicolon) + + @property + def name(self) -> ASTNestedName: + return self.declaration.name + + @property + def function_params(self) -> list[ASTFunctionParameter]: + if self.objectType != 'function': + return None + return self.declaration.function_params + + def get_id(self, version: int, prefixed: bool = True) -> str: + if version == 1: + if self.templatePrefix or self.trailingRequiresClause: + raise NoOldIdError + if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: + return self.enumeratorScopedSymbol.declaration.get_id(version) + return self.declaration.get_id(version, self.objectType, self.symbol) + # version >= 2 + if self.objectType == 'enumerator' and self.enumeratorScopedSymbol: + return self.enumeratorScopedSymbol.declaration.get_id(version, prefixed) + if prefixed: + res = [_id_prefix[version]] + else: + res = [] + # (See also https://github.com/sphinx-doc/sphinx/pull/10286#issuecomment-1168102147) + # The first implementation of requires clauses only supported a single clause after the + # template prefix, and no trailing clause. It put the ID after the template parameter + # list, i.e., + # "I" + template_parameter_list_id + "E" + "IQ" + requires_clause_id + "E" + # but the second implementation associates the requires clause with each list, i.e., + # "I" + template_parameter_list_id + "IQ" + requires_clause_id + "E" + "E" + # To avoid making a new ID version, we make an exception for the last requires clause + # in the template prefix, and still put it in the end. + # As we now support trailing requires clauses we add that as if it was a conjunction. + if self.templatePrefix is not None: + res.append(self.templatePrefix.get_id_except_requires_clause_in_last(version)) + requiresClauseInLast = self.templatePrefix.get_requires_clause_in_last() + else: + requiresClauseInLast = None + + if requiresClauseInLast or self.trailingRequiresClause: + if version < 4: + raise NoOldIdError + res.append('IQ') + if requiresClauseInLast and self.trailingRequiresClause: + # make a conjunction of them + res.append('aa') + if requiresClauseInLast: + res.append(requiresClauseInLast.expr.get_id(version)) + if self.trailingRequiresClause: + res.append(self.trailingRequiresClause.expr.get_id(version)) + res.append('E') + res.append(self.declaration.get_id(version, self.objectType, self.symbol)) + return ''.join(res) + + def get_newest_id(self) -> str: + return self.get_id(_max_id, True) + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.visibility and self.visibility != "public": + res.append(self.visibility) + res.append(' ') + if self.templatePrefix: + res.append(transform(self.templatePrefix)) + res.append(transform(self.declaration)) + if self.trailingRequiresClause: + res.append(' ') + res.append(transform(self.trailingRequiresClause)) + if self.semicolon: + res.append(';') + return ''.join(res) + + def describe_signature(self, signode: desc_signature, mode: str, + env: BuildEnvironment, options: dict) -> None: + verify_description_mode(mode) + assert self.symbol + # The caller of the domain added a desc_signature node. + # Always enable multiline: + signode['is_multiline'] = True + # Put each line in a desc_signature_line node. + mainDeclNode = addnodes.desc_signature_line() + mainDeclNode.sphinx_line_type = 'declarator' + mainDeclNode['add_permalink'] = not self.symbol.isRedeclaration + + if self.templatePrefix: + self.templatePrefix.describe_signature(signode, mode, env, + symbol=self.symbol, + lineSpec=options.get('tparam-line-spec')) + signode += mainDeclNode + if self.visibility and self.visibility != "public": + mainDeclNode += addnodes.desc_sig_keyword(self.visibility, self.visibility) + mainDeclNode += addnodes.desc_sig_space() + if self.objectType == 'type': + prefix = self.declaration.get_type_declaration_prefix() + mainDeclNode += addnodes.desc_sig_keyword(prefix, prefix) + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'concept': + mainDeclNode += addnodes.desc_sig_keyword('concept', 'concept') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType in {'member', 'function'}: + pass + elif self.objectType == 'class': + assert self.directiveType in ('class', 'struct') + mainDeclNode += addnodes.desc_sig_keyword(self.directiveType, self.directiveType) + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'union': + mainDeclNode += addnodes.desc_sig_keyword('union', 'union') + mainDeclNode += addnodes.desc_sig_space() + elif self.objectType == 'enum': + mainDeclNode += addnodes.desc_sig_keyword('enum', 'enum') + mainDeclNode += addnodes.desc_sig_space() + if self.directiveType == 'enum-class': + mainDeclNode += addnodes.desc_sig_keyword('class', 'class') + mainDeclNode += addnodes.desc_sig_space() + elif self.directiveType == 'enum-struct': + mainDeclNode += addnodes.desc_sig_keyword('struct', 'struct') + mainDeclNode += addnodes.desc_sig_space() + else: + assert self.directiveType == 'enum', self.directiveType + elif self.objectType == 'enumerator': + mainDeclNode += addnodes.desc_sig_keyword('enumerator', 'enumerator') + mainDeclNode += addnodes.desc_sig_space() + else: + raise AssertionError(self.objectType) + self.declaration.describe_signature(mainDeclNode, mode, env, self.symbol) + lastDeclNode = mainDeclNode + if self.trailingRequiresClause: + trailingReqNode = addnodes.desc_signature_line() + trailingReqNode.sphinx_line_type = 'trailingRequiresClause' + signode.append(trailingReqNode) + lastDeclNode = trailingReqNode + self.trailingRequiresClause.describe_signature( + trailingReqNode, 'markType', env, self.symbol) + if self.semicolon: + lastDeclNode += addnodes.desc_sig_punctuation(';', ';') + + +class ASTNamespace(ASTBase): + def __init__(self, nestedName: ASTNestedName, + templatePrefix: ASTTemplateDeclarationPrefix) -> None: + self.nestedName = nestedName + self.templatePrefix = templatePrefix + + def _stringify(self, transform: StringifyTransform) -> str: + res = [] + if self.templatePrefix: + res.append(transform(self.templatePrefix)) + res.append(transform(self.nestedName)) + return ''.join(res) + + +class SymbolLookupResult: + def __init__(self, symbols: Iterator[Symbol], parentSymbol: Symbol, + identOrOp: ASTIdentifier | ASTOperator, templateParams: Any, + templateArgs: ASTTemplateArgs) -> None: + self.symbols = symbols + self.parentSymbol = parentSymbol + self.identOrOp = identOrOp + self.templateParams = templateParams + self.templateArgs = templateArgs + + +class LookupKey: + def __init__(self, data: list[tuple[ASTNestedNameElement, + ASTTemplateParams | ASTTemplateIntroduction, + str]]) -> None: + self.data = data + + +def _is_specialization(templateParams: ASTTemplateParams | ASTTemplateIntroduction, + templateArgs: ASTTemplateArgs) -> bool: + # Checks if `templateArgs` does not exactly match `templateParams`. + # the names of the template parameters must be given exactly as args + # and params that are packs must in the args be the name expanded + if len(templateParams.params) != len(templateArgs.args): + return True + # having no template params and no arguments is also a specialization + if len(templateParams.params) == 0: + return True + for i in range(len(templateParams.params)): + param = templateParams.params[i] + arg = templateArgs.args[i] + # TODO: doing this by string manipulation is probably not the most efficient + paramName = str(param.name) + argTxt = str(arg) + isArgPackExpansion = argTxt.endswith('...') + if param.isPack != isArgPackExpansion: + return True + argName = argTxt[:-3] if isArgPackExpansion else argTxt + if paramName != argName: + return True + return False + + +class Symbol: + debug_indent = 0 + debug_indent_string = " " + debug_lookup = False # overridden by the corresponding config value + debug_show_tree = False # overridden by the corresponding config value + + def __copy__(self): + raise AssertionError # shouldn't happen + + def __deepcopy__(self, memo): + if self.parent: + raise AssertionError # shouldn't happen + # the domain base class makes a copy of the initial data, which is fine + return Symbol(None, None, None, None, None, None, None) + + @staticmethod + def debug_print(*args: Any) -> None: + logger.debug(Symbol.debug_indent_string * Symbol.debug_indent, end="") + logger.debug(*args) + + def _assert_invariants(self) -> None: + if not self.parent: + # parent == None means global scope, so declaration means a parent + assert not self.identOrOp + assert not self.templateParams + assert not self.templateArgs + assert not self.declaration + assert not self.docname + else: + if self.declaration: + assert self.docname + + def __setattr__(self, key: str, value: Any) -> None: + if key == "children": + raise AssertionError + return super().__setattr__(key, value) + + def __init__(self, parent: Symbol | None, + identOrOp: ASTIdentifier | ASTOperator | None, + templateParams: ASTTemplateParams | ASTTemplateIntroduction | None, + templateArgs: Any, declaration: ASTDeclaration | None, + docname: str | None, line: int | None) -> None: + self.parent = parent + # declarations in a single directive are linked together + self.siblingAbove: Symbol | None = None + self.siblingBelow: Symbol | None = None + self.identOrOp = identOrOp + # Ensure the same symbol for `A` is created for: + # + # .. cpp:class:: template <typename T> class A + # + # and + # + # .. cpp:function:: template <typename T> int A<T>::foo() + if (templateArgs is not None and + not _is_specialization(templateParams, templateArgs)): + templateArgs = None + self.templateParams = templateParams # template<templateParams> + self.templateArgs = templateArgs # identifier<templateArgs> + self.declaration = declaration + self.docname = docname + self.line = line + self.isRedeclaration = False + self._assert_invariants() + + # Remember to modify Symbol.remove if modifications to the parent change. + self._children: list[Symbol] = [] + self._anonChildren: list[Symbol] = [] + # note: _children includes _anonChildren + if self.parent: + self.parent._children.append(self) + if self.declaration: + self.declaration.symbol = self + + # Do symbol addition after self._children has been initialised. + self._add_template_and_function_params() + + def _fill_empty(self, declaration: ASTDeclaration, docname: str, line: int) -> None: + self._assert_invariants() + assert self.declaration is None + assert self.docname is None + assert self.line is None + assert declaration is not None + assert docname is not None + assert line is not None + self.declaration = declaration + self.declaration.symbol = self + self.docname = docname + self.line = line + self._assert_invariants() + # and symbol addition should be done as well + self._add_template_and_function_params() + + def _add_template_and_function_params(self) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_template_and_function_params:") + # Note: we may be called from _fill_empty, so the symbols we want + # to add may actually already be present (as empty symbols). + + # add symbols for the template params + if self.templateParams: + for tp in self.templateParams.params: + if not tp.get_identifier(): + continue + # only add a declaration if we our self are from a declaration + if self.declaration: + decl = ASTDeclaration(objectType='templateParam', declaration=tp) + else: + decl = None + nne = ASTNestedNameElement(tp.get_identifier(), None) + nn = ASTNestedName([nne], [False], rooted=False) + self._add_symbols(nn, [], decl, self.docname, self.line) + # add symbols for function parameters, if any + if self.declaration is not None and self.declaration.function_params is not None: + for fp in self.declaration.function_params: + if fp.arg is None: + continue + nn = fp.arg.name + if nn is None: + continue + # (comparing to the template params: we have checked that we are a declaration) + decl = ASTDeclaration(objectType='functionParam', declaration=fp) + assert not nn.rooted + assert len(nn.names) == 1 + self._add_symbols(nn, [], decl, self.docname, self.line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + + def remove(self) -> None: + if self.parent is None: + return + assert self in self.parent._children + self.parent._children.remove(self) + self.parent = None + + def clear_doc(self, docname: str) -> None: + newChildren: list[Symbol] = [] + for sChild in self._children: + sChild.clear_doc(docname) + if sChild.declaration and sChild.docname == docname: + sChild.declaration = None + sChild.docname = None + sChild.line = None + if sChild.siblingAbove is not None: + sChild.siblingAbove.siblingBelow = sChild.siblingBelow + if sChild.siblingBelow is not None: + sChild.siblingBelow.siblingAbove = sChild.siblingAbove + sChild.siblingAbove = None + sChild.siblingBelow = None + newChildren.append(sChild) + self._children = newChildren + + def get_all_symbols(self) -> Iterator[Any]: + yield self + for sChild in self._children: + yield from sChild.get_all_symbols() + + @property + def children_recurse_anon(self) -> Generator[Symbol, None, None]: + for c in self._children: + yield c + if not c.identOrOp.is_anon(): + continue + + yield from c.children_recurse_anon + + def get_lookup_key(self) -> LookupKey: + # The pickle files for the environment and for each document are distinct. + # The environment has all the symbols, but the documents has xrefs that + # must know their scope. A lookup key is essentially a specification of + # how to find a specific symbol. + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + key = [] + for s in symbols: + nne = ASTNestedNameElement(s.identOrOp, s.templateArgs) + if s.declaration is not None: + key.append((nne, s.templateParams, s.declaration.get_newest_id())) + else: + key.append((nne, s.templateParams, None)) + return LookupKey(key) + + def get_full_nested_name(self) -> ASTNestedName: + symbols = [] + s = self + while s.parent: + symbols.append(s) + s = s.parent + symbols.reverse() + names = [] + templates = [] + for s in symbols: + names.append(ASTNestedNameElement(s.identOrOp, s.templateArgs)) + templates.append(False) + return ASTNestedName(names, templates, rooted=False) + + def _find_first_named_symbol(self, identOrOp: ASTIdentifier | ASTOperator, + templateParams: Any, templateArgs: ASTTemplateArgs, + templateShorthand: bool, matchSelf: bool, + recurseInAnon: bool, correctPrimaryTemplateArgs: bool, + ) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_print("_find_first_named_symbol ->") + res = self._find_named_symbols(identOrOp, templateParams, templateArgs, + templateShorthand, matchSelf, recurseInAnon, + correctPrimaryTemplateArgs, + searchInSiblings=False) + try: + return next(res) + except StopIteration: + return None + + def _find_named_symbols(self, identOrOp: ASTIdentifier | ASTOperator, + templateParams: Any, templateArgs: ASTTemplateArgs, + templateShorthand: bool, matchSelf: bool, + recurseInAnon: bool, correctPrimaryTemplateArgs: bool, + searchInSiblings: bool) -> Iterator[Symbol]: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_find_named_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("templateParams: ", templateParams) + Symbol.debug_print("templateArgs: ", templateArgs) + Symbol.debug_print("templateShorthand: ", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("correctPrimaryTemplateAargs:", correctPrimaryTemplateArgs) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + if correctPrimaryTemplateArgs: + if templateParams is not None and templateArgs is not None: + # If both are given, but it's not a specialization, then do lookup as if + # there is no argument list. + # For example: template<typename T> int A<T>::var; + if not _is_specialization(templateParams, templateArgs): + templateArgs = None + + def matches(s: Symbol) -> bool: + if s.identOrOp != identOrOp: + return False + if (s.templateParams is None) != (templateParams is None): + if templateParams is not None: + # we query with params, they must match params + return False + if not templateShorthand: + # we don't query with params, and we do care about them + return False + if templateParams: + # TODO: do better comparison + if str(s.templateParams) != str(templateParams): + return False + if (s.templateArgs is None) != (templateArgs is None): + return False + if s.templateArgs: + # TODO: do better comparison + if str(s.templateArgs) != str(templateArgs): + return False + return True + + def candidates() -> Generator[Symbol, None, None]: + s = self + if Symbol.debug_lookup: + Symbol.debug_print("searching in self:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + while True: + if matchSelf: + yield s + if recurseInAnon: + yield from s.children_recurse_anon + else: + yield from s._children + + if s.siblingAbove is None: + break + s = s.siblingAbove + if Symbol.debug_lookup: + Symbol.debug_print("searching in sibling:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + + for s in candidates(): + if Symbol.debug_lookup: + Symbol.debug_print("candidate:") + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + if matches(s): + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("matches") + Symbol.debug_indent -= 3 + yield s + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + + def _symbol_lookup( + self, + nestedName: ASTNestedName, + templateDecls: list[Any], + onMissingQualifiedSymbol: Callable[ + [Symbol, ASTIdentifier | ASTOperator, Any, ASTTemplateArgs], Symbol | None, + ], + strictTemplateParamArgLists: bool, ancestorLookupType: str, + templateShorthand: bool, matchSelf: bool, + recurseInAnon: bool, correctPrimaryTemplateArgs: bool, + searchInSiblings: bool, + ) -> SymbolLookupResult: + # ancestorLookupType: if not None, specifies the target type of the lookup + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_symbol_lookup:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("templateDecls: ", ",".join(str(t) for t in templateDecls)) + Symbol.debug_print("strictTemplateParamArgLists:", strictTemplateParamArgLists) + Symbol.debug_print("ancestorLookupType:", ancestorLookupType) + Symbol.debug_print("templateShorthand: ", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("correctPrimaryTemplateArgs: ", correctPrimaryTemplateArgs) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + if strictTemplateParamArgLists: + # Each template argument list must have a template parameter list. + # But to declare a template there must be an additional template parameter list. + assert (nestedName.num_templates() == len(templateDecls) or + nestedName.num_templates() + 1 == len(templateDecls)) + else: + assert len(templateDecls) <= nestedName.num_templates() + 1 + + names = nestedName.names + + # find the right starting point for lookup + parentSymbol = self + if nestedName.rooted: + while parentSymbol.parent: + parentSymbol = parentSymbol.parent + if ancestorLookupType is not None: + # walk up until we find the first identifier + firstName = names[0] + if not firstName.is_operator(): + while parentSymbol.parent: + if parentSymbol.find_identifier(firstName.identOrOp, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + searchInSiblings=searchInSiblings): + # if we are in the scope of a constructor but wants to + # reference the class we need to walk one extra up + if (len(names) == 1 and ancestorLookupType == 'class' and matchSelf and + parentSymbol.parent and + parentSymbol.parent.identOrOp == firstName.identOrOp): + pass + else: + break + parentSymbol = parentSymbol.parent + + if Symbol.debug_lookup: + Symbol.debug_print("starting point:") + logger.debug(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # and now the actual lookup + iTemplateDecl = 0 + for name in names[:-1]: + identOrOp = name.identOrOp + templateArgs = name.templateArgs + if strictTemplateParamArgLists: + # there must be a parameter list + if templateArgs: + assert iTemplateDecl < len(templateDecls) + templateParams = templateDecls[iTemplateDecl] + iTemplateDecl += 1 + else: + templateParams = None + else: + # take the next template parameter list if there is one + # otherwise it's ok + if templateArgs and iTemplateDecl < len(templateDecls): + templateParams = templateDecls[iTemplateDecl] + iTemplateDecl += 1 + else: + templateParams = None + + symbol = parentSymbol._find_first_named_symbol( + identOrOp, + templateParams, templateArgs, + templateShorthand=templateShorthand, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + correctPrimaryTemplateArgs=correctPrimaryTemplateArgs) + if symbol is None: + symbol = onMissingQualifiedSymbol(parentSymbol, identOrOp, + templateParams, templateArgs) + if symbol is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + # We have now matched part of a nested name, and need to match more + # so even if we should matchSelf before, we definitely shouldn't + # even more. (see also issue #2666) + matchSelf = False + parentSymbol = symbol + + if Symbol.debug_lookup: + Symbol.debug_print("handle last name from:") + logger.debug(parentSymbol.to_string(Symbol.debug_indent + 1), end="") + + # handle the last name + name = names[-1] + identOrOp = name.identOrOp + templateArgs = name.templateArgs + if iTemplateDecl < len(templateDecls): + assert iTemplateDecl + 1 == len(templateDecls) + templateParams = templateDecls[iTemplateDecl] + else: + assert iTemplateDecl == len(templateDecls) + templateParams = None + + symbols = parentSymbol._find_named_symbols( + identOrOp, templateParams, templateArgs, + templateShorthand=templateShorthand, matchSelf=matchSelf, + recurseInAnon=recurseInAnon, correctPrimaryTemplateArgs=False, + searchInSiblings=searchInSiblings) + if Symbol.debug_lookup: + symbols = list(symbols) # type: ignore[assignment] + Symbol.debug_indent -= 2 + return SymbolLookupResult(symbols, parentSymbol, + identOrOp, templateParams, templateArgs) + + def _add_symbols(self, nestedName: ASTNestedName, templateDecls: list[Any], + declaration: ASTDeclaration, docname: str, line: int) -> Symbol: + # Used for adding a whole path of symbols, where the last may or may not + # be an actual declaration. + + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("tdecls:", ",".join(str(t) for t in templateDecls)) + Symbol.debug_print("nn: ", nestedName) + Symbol.debug_print("decl: ", declaration) + Symbol.debug_print(f"location: {docname}:{line}") + + def onMissingQualifiedSymbol(parentSymbol: Symbol, + identOrOp: ASTIdentifier | ASTOperator, + templateParams: Any, templateArgs: ASTTemplateArgs, + ) -> Symbol | None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("_add_symbols, onMissingQualifiedSymbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("templateParams:", templateParams) + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("templateARgs: ", templateArgs) + Symbol.debug_indent -= 2 + return Symbol(parent=parentSymbol, identOrOp=identOrOp, + templateParams=templateParams, + templateArgs=templateArgs, declaration=None, + docname=None, line=None) + + lookupResult = self._symbol_lookup(nestedName, templateDecls, + onMissingQualifiedSymbol, + strictTemplateParamArgLists=True, + ancestorLookupType=None, + templateShorthand=False, + matchSelf=False, + recurseInAnon=False, + correctPrimaryTemplateArgs=True, + searchInSiblings=False) + assert lookupResult is not None # we create symbols all the way, so that can't happen + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, no symbol:") + Symbol.debug_indent += 1 + Symbol.debug_print("templateParams:", lookupResult.templateParams) + Symbol.debug_print("identOrOp: ", lookupResult.identOrOp) + Symbol.debug_print("templateArgs: ", lookupResult.templateArgs) + Symbol.debug_print("declaration: ", declaration) + Symbol.debug_print(f"location: {docname}:{line}") + Symbol.debug_indent -= 1 + symbol = Symbol(parent=lookupResult.parentSymbol, + identOrOp=lookupResult.identOrOp, + templateParams=lookupResult.templateParams, + templateArgs=lookupResult.templateArgs, + declaration=declaration, + docname=docname, line=line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return symbol + + if Symbol.debug_lookup: + Symbol.debug_print("_add_symbols, result, symbols:") + Symbol.debug_indent += 1 + Symbol.debug_print("number symbols:", len(symbols)) + Symbol.debug_indent -= 1 + + if not declaration: + if Symbol.debug_lookup: + Symbol.debug_print("no declaration") + Symbol.debug_indent -= 2 + # good, just a scope creation + # TODO: what if we have more than one symbol? + return symbols[0] + + noDecl = [] + withDecl = [] + dupDecl = [] + for s in symbols: + if s.declaration is None: + noDecl.append(s) + elif s.isRedeclaration: + dupDecl.append(s) + else: + withDecl.append(s) + if Symbol.debug_lookup: + Symbol.debug_print("#noDecl: ", len(noDecl)) + Symbol.debug_print("#withDecl:", len(withDecl)) + Symbol.debug_print("#dupDecl: ", len(dupDecl)) + # With partial builds we may start with a large symbol tree stripped of declarations. + # Essentially any combination of noDecl, withDecl, and dupDecls seems possible. + # TODO: make partial builds fully work. What should happen when the primary symbol gets + # deleted, and other duplicates exist? The full document should probably be rebuild. + + # First check if one of those with a declaration matches. + # If it's a function, we need to compare IDs, + # otherwise there should be only one symbol with a declaration. + def makeCandSymbol() -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_print("begin: creating candidate symbol") + symbol = Symbol(parent=lookupResult.parentSymbol, + identOrOp=lookupResult.identOrOp, + templateParams=lookupResult.templateParams, + templateArgs=lookupResult.templateArgs, + declaration=declaration, + docname=docname, line=line) + if Symbol.debug_lookup: + Symbol.debug_print("end: creating candidate symbol") + return symbol + if len(withDecl) == 0: + candSymbol = None + else: + candSymbol = makeCandSymbol() + + def handleDuplicateDeclaration(symbol: Symbol, candSymbol: Symbol) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("redeclaration") + Symbol.debug_indent -= 1 + Symbol.debug_indent -= 2 + # Redeclaration of the same symbol. + # Let the new one be there, but raise an error to the client + # so it can use the real symbol as subscope. + # This will probably result in a duplicate id warning. + 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: + # but all existing must be functions as well, + # otherwise we declare it to be a duplicate + if symbol.declaration.objectType != 'function': + handleDuplicateDeclaration(symbol, candSymbol) + # (not reachable) + 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 + # if there is an empty symbol, fill that one + if len(noDecl) == 0: + if Symbol.debug_lookup: + Symbol.debug_print("no match, no empty") + if candSymbol is not None: + Symbol.debug_print("result is already created candSymbol") + else: + Symbol.debug_print("result is makeCandSymbol()") + Symbol.debug_indent -= 2 + if candSymbol is not None: + return candSymbol + else: + return makeCandSymbol() + else: + if Symbol.debug_lookup: + Symbol.debug_print( + "no match, but fill an empty declaration, candSybmol is not None?:", + candSymbol is not None, + ) + Symbol.debug_indent -= 2 + if candSymbol is not None: + candSymbol.remove() + # assert len(noDecl) == 1 + # TODO: enable assertion when we at some point find out how to do cleanup + # for now, just take the first one, it should work fine ... right? + symbol = noDecl[0] + # If someone first opened the scope, and then later + # declares it, e.g, + # .. namespace:: Test + # .. namespace:: nullptr + # .. class:: Test + symbol._fill_empty(declaration, docname, line) + return symbol + + def merge_with(self, other: Symbol, docnames: list[str], + env: BuildEnvironment) -> None: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("merge_with:") + assert other is not None + + def unconditionalAdd(self, otherChild): + # TODO: hmm, should we prune by docnames? + self._children.append(otherChild) + otherChild.parent = self + otherChild._assert_invariants() + + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + for otherChild in other._children: + if Symbol.debug_lookup: + Symbol.debug_print("otherChild:\n", otherChild.to_string(Symbol.debug_indent)) + Symbol.debug_indent += 1 + if otherChild.isRedeclaration: + unconditionalAdd(self, otherChild) + if Symbol.debug_lookup: + Symbol.debug_print("isRedeclaration") + Symbol.debug_indent -= 1 + continue + candiateIter = self._find_named_symbols( + identOrOp=otherChild.identOrOp, + templateParams=otherChild.templateParams, + templateArgs=otherChild.templateArgs, + templateShorthand=False, matchSelf=False, + recurseInAnon=False, correctPrimaryTemplateArgs=False, + searchInSiblings=False) + candidates = list(candiateIter) + + if Symbol.debug_lookup: + Symbol.debug_print("raw candidate symbols:", len(candidates)) + symbols = [s for s in candidates if not s.isRedeclaration] + if Symbol.debug_lookup: + Symbol.debug_print("non-duplicate candidate symbols:", len(symbols)) + + if len(symbols) == 0: + unconditionalAdd(self, otherChild) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + continue + + ourChild = None + if otherChild.declaration is None: + if Symbol.debug_lookup: + Symbol.debug_print("no declaration in other child") + ourChild = symbols[0] + else: + queryId = otherChild.declaration.get_newest_id() + if Symbol.debug_lookup: + Symbol.debug_print("queryId: ", queryId) + for symbol in symbols: + if symbol.declaration is None: + if Symbol.debug_lookup: + Symbol.debug_print("empty candidate") + # if in the end we have non-matching, but have an empty one, + # then just continue with that + ourChild = symbol + continue + candId = symbol.declaration.get_newest_id() + if Symbol.debug_lookup: + Symbol.debug_print("candidate:", candId) + if candId == queryId: + ourChild = symbol + break + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + if ourChild is None: + unconditionalAdd(self, otherChild) + continue + if otherChild.declaration and otherChild.docname in docnames: + if not ourChild.declaration: + ourChild._fill_empty(otherChild.declaration, + otherChild.docname, otherChild.line) + elif ourChild.docname != otherChild.docname: + name = str(ourChild.declaration) + msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" + "Declaration is '.. cpp:%s:: %s'.") + msg = msg % (ourChild.docname, ourChild.line, + ourChild.declaration.directiveType, name) + logger.warning(msg, location=(otherChild.docname, otherChild.line)) + else: + if (otherChild.declaration.objectType == + ourChild.declaration.objectType and + otherChild.declaration.objectType in + ('templateParam', 'functionParam') and + ourChild.parent.declaration == otherChild.parent.declaration): + # `ourChild` was just created during merging by the call + # to `_fill_empty` on the parent and can be ignored. + pass + else: + # Both have declarations, and in the same docname. + # This can apparently happen, it should be safe to + # just ignore it, right? + # Hmm, only on duplicate declarations, right? + msg = "Internal C++ domain error during symbol merging.\n" + msg += "ourChild:\n" + ourChild.to_string(1) + msg += "\notherChild:\n" + otherChild.to_string(1) + logger.warning(msg, location=otherChild.docname) + ourChild.merge_with(otherChild, docnames, env) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + + def add_name(self, nestedName: ASTNestedName, + templatePrefix: ASTTemplateDeclarationPrefix | None = None) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_name:") + if templatePrefix: + templateDecls = templatePrefix.templates + else: + templateDecls = [] + res = self._add_symbols(nestedName, templateDecls, + declaration=None, docname=None, line=None) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def add_declaration(self, declaration: ASTDeclaration, + docname: str, line: int) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("add_declaration:") + assert declaration is not None + assert docname is not None + assert line is not None + nestedName = declaration.name + if declaration.templatePrefix: + templateDecls = declaration.templatePrefix.templates + else: + templateDecls = [] + res = self._add_symbols(nestedName, templateDecls, declaration, docname, line) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + return res + + def find_identifier(self, identOrOp: ASTIdentifier | ASTOperator, + matchSelf: bool, recurseInAnon: bool, searchInSiblings: bool, + ) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_identifier:") + Symbol.debug_indent += 1 + Symbol.debug_print("identOrOp: ", identOrOp) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings:", searchInSiblings) + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + current = self + while current is not None: + if Symbol.debug_lookup: + Symbol.debug_indent += 2 + Symbol.debug_print("trying:") + logger.debug(current.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_indent -= 2 + if matchSelf and current.identOrOp == identOrOp: + return current + children = current.children_recurse_anon if recurseInAnon else current._children + for s in children: + if s.identOrOp == identOrOp: + return s + if not searchInSiblings: + break + current = current.siblingAbove + return None + + def direct_lookup(self, key: LookupKey) -> Symbol: + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("direct_lookup:") + Symbol.debug_indent += 1 + s = self + for name, templateParams, id_ in key.data: + if id_ is not None: + res = None + for cand in s._children: + if cand.declaration is None: + continue + if cand.declaration.get_newest_id() == id_: + res = cand + break + s = res + else: + identOrOp = name.identOrOp + templateArgs = name.templateArgs + s = s._find_first_named_symbol(identOrOp, + templateParams, templateArgs, + templateShorthand=False, + matchSelf=False, + recurseInAnon=False, + correctPrimaryTemplateArgs=False) + if Symbol.debug_lookup: + Symbol.debug_print("name: ", name) + Symbol.debug_print("templateParams:", templateParams) + Symbol.debug_print("id: ", id_) + if s is not None: + logger.debug(s.to_string(Symbol.debug_indent + 1), end="") + else: + Symbol.debug_print("not found") + if s is None: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return s + + def find_name(self, nestedName: ASTNestedName, templateDecls: list[Any], + typ: str, templateShorthand: bool, matchSelf: bool, + recurseInAnon: bool, searchInSiblings: bool) -> tuple[list[Symbol], str]: + # templateShorthand: missing template parameter lists for templates is ok + # If the first component is None, + # then the second component _may_ be a string explaining why. + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_name:") + Symbol.debug_indent += 1 + Symbol.debug_print("self:") + logger.debug(self.to_string(Symbol.debug_indent + 1), end="") + Symbol.debug_print("nestedName: ", nestedName) + Symbol.debug_print("templateDecls: ", templateDecls) + Symbol.debug_print("typ: ", typ) + Symbol.debug_print("templateShorthand:", templateShorthand) + Symbol.debug_print("matchSelf: ", matchSelf) + Symbol.debug_print("recurseInAnon: ", recurseInAnon) + Symbol.debug_print("searchInSiblings: ", searchInSiblings) + + class QualifiedSymbolIsTemplateParam(Exception): + pass + + def onMissingQualifiedSymbol(parentSymbol: Symbol, + identOrOp: ASTIdentifier | ASTOperator, + templateParams: Any, + templateArgs: ASTTemplateArgs) -> Symbol | None: + # TODO: Maybe search without template args? + # Though, the correctPrimaryTemplateArgs does + # that for primary templates. + # Is there another case where it would be good? + if parentSymbol.declaration is not None: + if parentSymbol.declaration.objectType == 'templateParam': + raise QualifiedSymbolIsTemplateParam + return None + + try: + lookupResult = self._symbol_lookup(nestedName, templateDecls, + onMissingQualifiedSymbol, + strictTemplateParamArgLists=False, + ancestorLookupType=typ, + templateShorthand=templateShorthand, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + correctPrimaryTemplateArgs=False, + searchInSiblings=searchInSiblings) + except QualifiedSymbolIsTemplateParam: + return None, "templateParamInQualified" + + if lookupResult is None: + # if it was a part of the qualification that could not be found + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return None, None + + res = list(lookupResult.symbols) + if len(res) != 0: + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + return res, None + + if lookupResult.parentSymbol.declaration is not None: + if lookupResult.parentSymbol.declaration.objectType == 'templateParam': + return None, "templateParamInQualified" + + # try without template params and args + symbol = lookupResult.parentSymbol._find_first_named_symbol( + lookupResult.identOrOp, None, None, + templateShorthand=templateShorthand, matchSelf=matchSelf, + recurseInAnon=recurseInAnon, correctPrimaryTemplateArgs=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 2 + if symbol is not None: + return [symbol], None + else: + return None, None + + def find_declaration(self, declaration: ASTDeclaration, typ: str, templateShorthand: bool, + matchSelf: bool, recurseInAnon: bool) -> Symbol: + # templateShorthand: missing template parameter lists for templates is ok + if Symbol.debug_lookup: + Symbol.debug_indent += 1 + Symbol.debug_print("find_declaration:") + nestedName = declaration.name + if declaration.templatePrefix: + templateDecls = declaration.templatePrefix.templates + else: + templateDecls = [] + + def onMissingQualifiedSymbol(parentSymbol: Symbol, + identOrOp: ASTIdentifier | ASTOperator, + templateParams: Any, + templateArgs: ASTTemplateArgs) -> Symbol | None: + return None + + lookupResult = self._symbol_lookup(nestedName, templateDecls, + onMissingQualifiedSymbol, + strictTemplateParamArgLists=False, + ancestorLookupType=typ, + templateShorthand=templateShorthand, + matchSelf=matchSelf, + recurseInAnon=recurseInAnon, + correctPrimaryTemplateArgs=False, + searchInSiblings=False) + if Symbol.debug_lookup: + Symbol.debug_indent -= 1 + if lookupResult is None: + return None + + symbols = list(lookupResult.symbols) + if len(symbols) == 0: + return None + + querySymbol = Symbol(parent=lookupResult.parentSymbol, + identOrOp=lookupResult.identOrOp, + templateParams=lookupResult.templateParams, + templateArgs=lookupResult.templateArgs, + declaration=declaration, + docname='fakeDocnameForQuery', + line=42) + queryId = declaration.get_newest_id() + for symbol in symbols: + if symbol.declaration is None: + continue + candId = symbol.declaration.get_newest_id() + if candId == queryId: + querySymbol.remove() + return symbol + querySymbol.remove() + return None + + def to_string(self, indent: int) -> str: + res = [Symbol.debug_indent_string * indent] + if not self.parent: + res.append('::') + else: + if self.templateParams: + res.append(str(self.templateParams)) + res.append('\n') + res.append(Symbol.debug_indent_string * indent) + if self.identOrOp: + res.append(str(self.identOrOp)) + else: + res.append(str(self.declaration)) + if self.templateArgs: + res.append(str(self.templateArgs)) + if self.declaration: + res.append(": ") + if self.isRedeclaration: + res.append('!!duplicate!! ') + res.append("{" + self.declaration.objectType + "} ") + res.append(str(self.declaration)) + if self.docname: + res.append('\t(') + res.append(self.docname) + res.append(')') + res.append('\n') + return ''.join(res) + + def dump(self, indent: int) -> str: + res = [self.to_string(indent)] + for c in self._children: + res.append(c.dump(indent + 1)) + return ''.join(res) + + +class DefinitionParser(BaseParser): + @property + def language(self) -> str: + return 'C++' + + @property + def id_attributes(self): + return self.config.cpp_id_attributes + + @property + def paren_attributes(self): + return self.config.cpp_paren_attributes + + def _parse_string(self) -> str: + if self.current_char != '"': + return None + startPos = self.pos + self.pos += 1 + escape = False + while True: + if self.eof: + self.fail("Unexpected end during inside string.") + elif self.current_char == '"' and not escape: + self.pos += 1 + break + elif self.current_char == '\\': + escape = True + else: + escape = False + self.pos += 1 + return self.definition[startPos:self.pos] + + def _parse_literal(self) -> ASTLiteral: + # -> integer-literal + # | character-literal + # | floating-literal + # | string-literal + # | boolean-literal -> "false" | "true" + # | pointer-literal -> "nullptr" + # | user-defined-literal + + def _udl(literal: ASTLiteral) -> ASTLiteral: + if not self.match(udl_identifier_re): + return literal + # hmm, should we care if it's a keyword? + # it looks like GCC does not disallow keywords + ident = ASTIdentifier(self.matched_text) + return ASTUserDefinedLiteral(literal, ident) + + self.skip_ws() + if self.skip_word('nullptr'): + return ASTPointerLiteral() + if self.skip_word('true'): + return ASTBooleanLiteral(True) + if self.skip_word('false'): + return ASTBooleanLiteral(False) + pos = self.pos + if self.match(float_literal_re): + hasSuffix = self.match(float_literal_suffix_re) + floatLit = ASTNumberLiteral(self.definition[pos:self.pos]) + if hasSuffix: + return floatLit + else: + return _udl(floatLit) + for regex in [binary_literal_re, hex_literal_re, + integer_literal_re, octal_literal_re]: + if self.match(regex): + hasSuffix = self.match(integers_literal_suffix_re) + intLit = ASTNumberLiteral(self.definition[pos:self.pos]) + if hasSuffix: + return intLit + else: + return _udl(intLit) + + string = self._parse_string() + if string is not None: + return _udl(ASTStringLiteral(string)) + + # character-literal + if self.match(char_literal_re): + prefix = self.last_match.group(1) # may be None when no prefix + data = self.last_match.group(2) + try: + charLit = ASTCharLiteral(prefix, data) + except UnicodeDecodeError as e: + self.fail("Can not handle character literal. Internal error was: %s" % e) + except UnsupportedMultiCharacterCharLiteral: + self.fail("Can not handle character literal" + " resulting in multiple decoded characters.") + return _udl(charLit) + return None + + def _parse_fold_or_paren_expression(self) -> ASTExpression: + # "(" expression ")" + # fold-expression + # -> ( cast-expression fold-operator ... ) + # | ( ... fold-operator cast-expression ) + # | ( cast-expression fold-operator ... fold-operator cast-expression + if self.current_char != '(': + return None + self.pos += 1 + self.skip_ws() + if self.skip_string_and_ws("..."): + # ( ... fold-operator cast-expression ) + if not self.match(_fold_operator_re): + self.fail("Expected fold operator after '...' in fold expression.") + op = self.matched_text + rightExpr = self._parse_cast_expression() + if not self.skip_string(')'): + self.fail("Expected ')' in end of fold expression.") + return ASTFoldExpr(None, op, rightExpr) + # try first parsing a unary right fold, or a binary fold + pos = self.pos + try: + self.skip_ws() + leftExpr = self._parse_cast_expression() + self.skip_ws() + if not self.match(_fold_operator_re): + self.fail("Expected fold operator after left expression in fold expression.") + op = self.matched_text + self.skip_ws() + if not self.skip_string_and_ws('...'): + self.fail("Expected '...' after fold operator in fold expression.") + except DefinitionError as eFold: + self.pos = pos + # fall back to a paren expression + try: + res = self._parse_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' in end of parenthesized expression.") + except DefinitionError as eExpr: + raise self._make_multi_error([ + (eFold, "If fold expression"), + (eExpr, "If parenthesized expression"), + ], "Error in fold expression or parenthesized expression.") from eExpr + return ASTParenExpr(res) + # now it definitely is a fold expression + if self.skip_string(')'): + return ASTFoldExpr(leftExpr, op, None) + if not self.match(_fold_operator_re): + self.fail("Expected fold operator or ')' after '...' in fold expression.") + if op != self.matched_text: + self.fail("Operators are different in binary fold: '%s' and '%s'." + % (op, self.matched_text)) + rightExpr = self._parse_cast_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' to end binary fold expression.") + return ASTFoldExpr(leftExpr, op, rightExpr) + + def _parse_primary_expression(self) -> ASTExpression: + # literal + # "this" + # lambda-expression + # "(" expression ")" + # fold-expression + # id-expression -> we parse this with _parse_nested_name + self.skip_ws() + res: ASTExpression = self._parse_literal() + if res is not None: + return res + self.skip_ws() + if self.skip_word("this"): + return ASTThisLiteral() + # TODO: try lambda expression + res = self._parse_fold_or_paren_expression() + if res is not None: + return res + nn = self._parse_nested_name() + if nn is not None: + return ASTIdExpression(nn) + return None + + def _parse_initializer_list(self, name: str, open: str, close: str, + ) -> tuple[list[ASTExpression | ASTBracedInitList], + bool]: + # Parse open and close with the actual initializer-list in between + # -> initializer-clause '...'[opt] + # | initializer-list ',' initializer-clause '...'[opt] + self.skip_ws() + if not self.skip_string_and_ws(open): + return None, None + if self.skip_string(close): + return [], False + + exprs: list[ASTExpression | ASTBracedInitList] = [] + trailingComma = False + while True: + self.skip_ws() + expr = self._parse_initializer_clause() + self.skip_ws() + if self.skip_string('...'): + exprs.append(ASTPackExpansionExpr(expr)) + else: + exprs.append(expr) + self.skip_ws() + if self.skip_string(close): + break + if not self.skip_string_and_ws(','): + self.fail(f"Error in {name}, expected ',' or '{close}'.") + if self.current_char == close and close == '}': + self.pos += 1 + trailingComma = True + break + return exprs, trailingComma + + def _parse_paren_expression_list(self) -> ASTParenExprList: + # -> '(' expression-list ')' + # though, we relax it to also allow empty parens + # as it's needed in some cases + # + # expression-list + # -> initializer-list + exprs, trailingComma = self._parse_initializer_list("parenthesized expression-list", + '(', ')') + if exprs is None: + return None + return ASTParenExprList(exprs) + + def _parse_initializer_clause(self) -> ASTExpression | ASTBracedInitList: + bracedInitList = self._parse_braced_init_list() + if bracedInitList is not None: + return bracedInitList + return self._parse_assignment_expression(inTemplate=False) + + def _parse_braced_init_list(self) -> ASTBracedInitList: + # -> '{' initializer-list ','[opt] '}' + # | '{' '}' + exprs, trailingComma = self._parse_initializer_list("braced-init-list", '{', '}') + if exprs is None: + return None + return ASTBracedInitList(exprs, trailingComma) + + def _parse_expression_list_or_braced_init_list( + self, + ) -> ASTParenExprList | ASTBracedInitList: + paren = self._parse_paren_expression_list() + if paren is not None: + return paren + return self._parse_braced_init_list() + + def _parse_postfix_expression(self) -> ASTPostfixExpr: + # -> primary + # | postfix "[" expression "]" + # | postfix "[" braced-init-list [opt] "]" + # | postfix "(" expression-list [opt] ")" + # | postfix "." "template" [opt] id-expression + # | postfix "->" "template" [opt] id-expression + # | postfix "." pseudo-destructor-name + # | postfix "->" pseudo-destructor-name + # | postfix "++" + # | postfix "--" + # | simple-type-specifier "(" expression-list [opt] ")" + # | simple-type-specifier braced-init-list + # | typename-specifier "(" expression-list [opt] ")" + # | typename-specifier braced-init-list + # | "dynamic_cast" "<" type-id ">" "(" expression ")" + # | "static_cast" "<" type-id ">" "(" expression ")" + # | "reinterpret_cast" "<" type-id ">" "(" expression ")" + # | "const_cast" "<" type-id ">" "(" expression ")" + # | "typeid" "(" expression ")" + # | "typeid" "(" type-id ")" + + prefixType = None + prefix: Any = None + self.skip_ws() + + cast = None + for c in _id_explicit_cast: + if self.skip_word_and_ws(c): + cast = c + break + if cast is not None: + prefixType = "cast" + if not self.skip_string("<"): + self.fail("Expected '<' after '%s'." % cast) + typ = self._parse_type(False) + self.skip_ws() + if not self.skip_string_and_ws(">"): + self.fail("Expected '>' after type in '%s'." % cast) + if not self.skip_string("("): + self.fail("Expected '(' in '%s'." % cast) + + def parser() -> ASTExpression: + return self._parse_expression() + expr = self._parse_expression_fallback([')'], parser) + self.skip_ws() + if not self.skip_string(")"): + self.fail("Expected ')' to end '%s'." % cast) + prefix = ASTExplicitCast(cast, typ, expr) + elif self.skip_word_and_ws("typeid"): + prefixType = "typeid" + if not self.skip_string_and_ws('('): + self.fail("Expected '(' after 'typeid'.") + pos = self.pos + try: + typ = self._parse_type(False) + prefix = ASTTypeId(typ, isType=True) + if not self.skip_string(')'): + self.fail("Expected ')' to end 'typeid' of type.") + except DefinitionError as eType: + self.pos = pos + try: + + def parser() -> ASTExpression: + return self._parse_expression() + expr = self._parse_expression_fallback([')'], parser) + prefix = ASTTypeId(expr, isType=False) + if not self.skip_string(')'): + self.fail("Expected ')' to end 'typeid' of expression.") + except DefinitionError as eExpr: + self.pos = pos + header = "Error in 'typeid(...)'." + header += " Expected type or expression." + errors = [] + errors.append((eType, "If type")) + errors.append((eExpr, "If expression")) + raise self._make_multi_error(errors, header) from eExpr + else: # a primary expression or a type + pos = self.pos + try: + prefix = self._parse_primary_expression() + prefixType = 'expr' + except DefinitionError as eOuter: + self.pos = pos + try: + # we are potentially casting, so save parens for us + # TODO: hmm, would we need to try both with operatorCast and with None? + prefix = self._parse_type(False, 'operatorCast') + prefixType = 'typeOperatorCast' + # | simple-type-specifier "(" expression-list [opt] ")" + # | simple-type-specifier braced-init-list + # | typename-specifier "(" expression-list [opt] ")" + # | typename-specifier braced-init-list + self.skip_ws() + if self.current_char != '(' and self.current_char != '{': + self.fail("Expecting '(' or '{' after type in cast expression.") + except DefinitionError as eInner: + self.pos = pos + header = "Error in postfix expression," + header += " expected primary expression or type." + errors = [] + errors.append((eOuter, "If primary expression")) + errors.append((eInner, "If type")) + raise self._make_multi_error(errors, header) from eInner + + # and now parse postfixes + postFixes: list[ASTPostfixOp] = [] + while True: + self.skip_ws() + if prefixType in ('expr', 'cast', 'typeid'): + if self.skip_string_and_ws('['): + expr = self._parse_expression() + self.skip_ws() + if not self.skip_string(']'): + 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() + postFixes.append(ASTPostfixMemberOfPointer(name)) + continue + if self.skip_string('++'): + postFixes.append(ASTPostfixInc()) + continue + if self.skip_string('--'): + postFixes.append(ASTPostfixDec()) + continue + lst = self._parse_expression_list_or_braced_init_list() + if lst is not None: + postFixes.append(ASTPostfixCallExpr(lst)) + continue + break + return ASTPostfixExpr(prefix, postFixes) + + def _parse_unary_expression(self) -> ASTExpression: + # -> postfix + # | "++" cast + # | "--" cast + # | unary-operator cast -> (* | & | + | - | ! | ~) cast + # The rest: + # | "sizeof" unary + # | "sizeof" "(" type-id ")" + # | "sizeof" "..." "(" identifier ")" + # | "alignof" "(" type-id ")" + # | noexcept-expression -> noexcept "(" expression ")" + # | new-expression + # | delete-expression + self.skip_ws() + for op in _expression_unary_ops: + # TODO: hmm, should we be able to backtrack here? + if op[0] in 'cn': + res = self.skip_word(op) + else: + res = self.skip_string(op) + if res: + expr = self._parse_cast_expression() + return ASTUnaryOpExpr(op, expr) + if self.skip_word_and_ws('sizeof'): + if self.skip_string_and_ws('...'): + if not self.skip_string_and_ws('('): + self.fail("Expecting '(' after 'sizeof...'.") + if not self.match(identifier_re): + self.fail("Expecting identifier for 'sizeof...'.") + ident = ASTIdentifier(self.matched_text) + self.skip_ws() + if not self.skip_string(")"): + self.fail("Expecting ')' to end 'sizeof...'.") + return ASTSizeofParamPack(ident) + if self.skip_string_and_ws('('): + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'sizeof'.") + return ASTSizeofType(typ) + expr = self._parse_unary_expression() + return ASTSizeofExpr(expr) + if self.skip_word_and_ws('alignof'): + if not self.skip_string_and_ws('('): + self.fail("Expecting '(' after 'alignof'.") + typ = self._parse_type(named=False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'alignof'.") + return ASTAlignofExpr(typ) + if self.skip_word_and_ws('noexcept'): + if not self.skip_string_and_ws('('): + self.fail("Expecting '(' after 'noexcept'.") + expr = self._parse_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'noexcept'.") + return ASTNoexceptExpr(expr) + # new-expression + pos = self.pos + rooted = self.skip_string('::') + self.skip_ws() + if not self.skip_word_and_ws('new'): + self.pos = pos + else: + # new-placement[opt] new-type-id new-initializer[opt] + # new-placement[opt] ( type-id ) new-initializer[opt] + isNewTypeId = True + if self.skip_string_and_ws('('): + # either this is a new-placement or it's the second production + # without placement, and it's actually the ( type-id ) part + self.fail("Sorry, neither new-placement nor parenthesised type-id " + "in new-epression is supported yet.") + # set isNewTypeId = False if it's (type-id) + if isNewTypeId: + declSpecs = self._parse_decl_specs(outer=None) + decl = self._parse_declarator(named=False, paramMode="new") + else: + self.fail("Sorry, parenthesised type-id in new expression not yet supported.") + lst = self._parse_expression_list_or_braced_init_list() + return ASTNewExpr(rooted, isNewTypeId, ASTType(declSpecs, decl), lst) + # delete-expression + pos = self.pos + rooted = self.skip_string('::') + self.skip_ws() + if not self.skip_word_and_ws('delete'): + self.pos = pos + else: + array = self.skip_string_and_ws('[') + if array and not self.skip_string_and_ws(']'): + self.fail("Expected ']' in array delete-expression.") + expr = self._parse_cast_expression() + return ASTDeleteExpr(rooted, array, expr) + return self._parse_postfix_expression() + + def _parse_cast_expression(self) -> ASTExpression: + # -> unary | "(" type-id ")" cast + pos = self.pos + self.skip_ws() + if self.skip_string('('): + try: + typ = self._parse_type(False) + if not self.skip_string(')'): + self.fail("Expected ')' in cast expression.") + expr = self._parse_cast_expression() + return ASTCastExpr(typ, expr) + except DefinitionError as exCast: + self.pos = pos + try: + return self._parse_unary_expression() + except DefinitionError as exUnary: + errs = [] + errs.append((exCast, "If type cast expression")) + errs.append((exUnary, "If unary expression")) + raise self._make_multi_error(errs, + "Error in cast expression.") from exUnary + else: + return self._parse_unary_expression() + + def _parse_logical_or_expression(self, inTemplate: bool) -> ASTExpression: + # logical-or = logical-and || + # logical-and = inclusive-or && + # inclusive-or = exclusive-or | + # exclusive-or = and ^ + # and = equality & + # equality = relational ==, != + # relational = shift <, >, <=, >=, <=> + # shift = additive <<, >> + # additive = multiplicative +, - + # multiplicative = pm *, /, % + # pm = cast .*, ->* + def _parse_bin_op_expr(self: DefinitionParser, + opId: int, inTemplate: bool) -> ASTExpression: + if opId + 1 == len(_expression_bin_ops): + def parser(inTemplate: bool) -> ASTExpression: + return self._parse_cast_expression() + else: + def parser(inTemplate: bool) -> ASTExpression: + return _parse_bin_op_expr(self, opId + 1, inTemplate=inTemplate) + exprs = [] + ops = [] + exprs.append(parser(inTemplate=inTemplate)) + while True: + self.skip_ws() + if inTemplate and self.current_char == '>': + break + pos = self.pos + oneMore = False + for op in _expression_bin_ops[opId]: + if op[0] in 'abcnox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue + if op == '&' and self.current_char == '&': + # don't split the && 'token' + self.pos -= 1 + # and btw. && has lower precedence, so we are done + break + try: + expr = parser(inTemplate=inTemplate) + exprs.append(expr) + ops.append(op) + oneMore = True + break + except DefinitionError: + self.pos = pos + if not oneMore: + break + return ASTBinOpExpr(exprs, ops) + return _parse_bin_op_expr(self, 0, inTemplate=inTemplate) + + def _parse_conditional_expression_tail(self, orExprHead: ASTExpression, + inTemplate: bool) -> ASTConditionalExpr | None: + # Consumes the orExprHead on success. + + # -> "?" expression ":" assignment-expression + self.skip_ws() + if not self.skip_string("?"): + return None + thenExpr = self._parse_expression() + self.skip_ws() + if not self.skip_string(":"): + self.fail('Expected ":" after then-expression in conditional expression.') + elseExpr = self._parse_assignment_expression(inTemplate) + return ASTConditionalExpr(orExprHead, thenExpr, elseExpr) + + def _parse_assignment_expression(self, inTemplate: bool) -> ASTExpression: + # -> conditional-expression + # | logical-or-expression assignment-operator initializer-clause + # | yield-expression -> "co_yield" assignment-expression + # | "co_yield" braced-init-list + # | throw-expression -> "throw" assignment-expression[opt] + # TODO: yield-expression + # TODO: throw-expression + + # Now we have (after expanding conditional-expression: + # logical-or-expression + # | logical-or-expression "?" expression ":" assignment-expression + # | logical-or-expression assignment-operator initializer-clause + leftExpr = self._parse_logical_or_expression(inTemplate=inTemplate) + # the ternary operator + condExpr = self._parse_conditional_expression_tail(leftExpr, inTemplate) + if condExpr is not None: + return condExpr + # and actual assignment + for op in _expression_assignment_ops: + if op[0] in 'anox': + if not self.skip_word(op): + continue + else: + if not self.skip_string(op): + continue + rightExpr = self._parse_initializer_clause() + return ASTAssignmentExpr(leftExpr, op, rightExpr) + # just a logical-or-expression + return leftExpr + + def _parse_constant_expression(self, inTemplate: bool) -> ASTExpression: + # -> conditional-expression -> + # logical-or-expression + # | logical-or-expression "?" expression ":" assignment-expression + orExpr = self._parse_logical_or_expression(inTemplate=inTemplate) + condExpr = self._parse_conditional_expression_tail(orExpr, inTemplate) + if condExpr is not None: + return condExpr + return orExpr + + def _parse_expression(self) -> ASTExpression: + # -> assignment-expression + # | expression "," assignment-expression + exprs = [self._parse_assignment_expression(inTemplate=False)] + while True: + self.skip_ws() + if not self.skip_string(','): + break + exprs.append(self._parse_assignment_expression(inTemplate=False)) + if len(exprs) == 1: + return exprs[0] + else: + return ASTCommaExpr(exprs) + + def _parse_expression_fallback(self, end: list[str], + parser: Callable[[], ASTExpression], + allow: bool = True) -> ASTExpression: + # Stupidly "parse" an expression. + # 'end' should be a list of characters which ends the expression. + + # first try to use the provided parser + prevPos = self.pos + try: + return parser() + except DefinitionError as e: + # some places (e.g., template parameters) we really don't want to use fallback, + # and for testing we may want to globally disable it + if not allow or not self.allowFallbackExpressionParsing: + raise + self.warn("Parsing of expression failed. Using fallback parser." + " Error was:\n%s" % e) + self.pos = prevPos + # and then the fallback scanning + assert end is not None + self.skip_ws() + startPos = self.pos + if self.match(_string_re): + value = self.matched_text + else: + # TODO: add handling of more bracket-like things, and quote handling + brackets = {'(': ')', '{': '}', '[': ']', '<': '>'} + symbols: list[str] = [] + while not self.eof: + if (len(symbols) == 0 and self.current_char in end): + break + if self.current_char in brackets: + symbols.append(brackets[self.current_char]) + elif len(symbols) > 0 and self.current_char == symbols[-1]: + symbols.pop() + self.pos += 1 + if len(end) > 0 and self.eof: + self.fail("Could not find end of expression starting at %d." + % startPos) + value = self.definition[startPos:self.pos].strip() + return ASTFallbackExpr(value.strip()) + + # ========================================================================== + + def _parse_operator(self) -> ASTOperator: + self.skip_ws() + # adapted from the old code + # yay, a regular operator definition + if self.match(_operator_re): + return ASTOperatorBuildIn(self.matched_text) + + # new/delete operator? + for op in 'new', 'delete': + if not self.skip_word(op): + continue + self.skip_ws() + if self.skip_string('['): + self.skip_ws() + if not self.skip_string(']'): + self.fail('Expected "]" after "operator ' + op + '["') + op += '[]' + return ASTOperatorBuildIn(op) + + # user-defined literal? + if self.skip_string('""'): + self.skip_ws() + if not self.match(identifier_re): + self.fail("Expected user-defined literal suffix.") + identifier = ASTIdentifier(self.matched_text) + return ASTOperatorLiteral(identifier) + + # oh well, looks like a cast operator definition. + # In that case, eat another type. + type = self._parse_type(named=False, outer="operatorCast") + return ASTOperatorType(type) + + def _parse_template_argument_list(self) -> ASTTemplateArgs: + # template-argument-list: (but we include the < and > here + # template-argument ...[opt] + # template-argument-list, template-argument ...[opt] + # template-argument: + # constant-expression + # type-id + # id-expression + self.skip_ws() + if not self.skip_string_and_ws('<'): + return None + if self.skip_string('>'): + return ASTTemplateArgs([], False) + prevErrors = [] + templateArgs: list[ASTType | ASTTemplateArgConstant] = [] + packExpansion = False + while 1: + pos = self.pos + parsedComma = False + parsedEnd = False + try: + type = self._parse_type(named=False) + self.skip_ws() + if self.skip_string_and_ws('...'): + packExpansion = True + parsedEnd = True + if not self.skip_string('>'): + self.fail('Expected ">" after "..." in template argument list.') + elif self.skip_string('>'): + parsedEnd = True + elif self.skip_string(','): + parsedComma = True + else: + self.fail('Expected "...>", ">" or "," in template argument list.') + templateArgs.append(type) + except DefinitionError as e: + prevErrors.append((e, "If type argument")) + self.pos = pos + try: + value = self._parse_constant_expression(inTemplate=True) + self.skip_ws() + if self.skip_string_and_ws('...'): + packExpansion = True + parsedEnd = True + if not self.skip_string('>'): + self.fail('Expected ">" after "..." in template argument list.') + elif self.skip_string('>'): + parsedEnd = True + elif self.skip_string(','): + parsedComma = True + else: + self.fail('Expected "...>", ">" or "," in template argument list.') + templateArgs.append(ASTTemplateArgConstant(value)) + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If non-type argument")) + header = "Error in parsing template argument list." + raise self._make_multi_error(prevErrors, header) from e + if parsedEnd: + assert not parsedComma + break + assert not packExpansion + return ASTTemplateArgs(templateArgs, packExpansion) + + def _parse_nested_name(self, memberPointer: bool = False) -> ASTNestedName: + names: list[ASTNestedNameElement] = [] + templates: list[bool] = [] + + self.skip_ws() + rooted = False + if self.skip_string('::'): + rooted = True + while 1: + self.skip_ws() + if len(names) > 0: + template = self.skip_word_and_ws('template') + else: + template = False + templates.append(template) + identOrOp: ASTIdentifier | ASTOperator = None + if self.skip_word_and_ws('operator'): + identOrOp = self._parse_operator() + else: + if not self.match(identifier_re): + if memberPointer and len(names) > 0: + templates.pop() + break + self.fail("Expected identifier in nested name.") + identifier = self.matched_text + # make sure there isn't a keyword + if identifier in _keywords: + self.fail("Expected identifier in nested name, " + "got keyword: %s" % identifier) + identOrOp = ASTIdentifier(identifier) + # try greedily to get template arguments, + # but otherwise a < might be because we are in an expression + pos = self.pos + try: + templateArgs = self._parse_template_argument_list() + except DefinitionError as ex: + self.pos = pos + templateArgs = None + self.otherErrors.append(ex) + names.append(ASTNestedNameElement(identOrOp, templateArgs)) + + self.skip_ws() + if not self.skip_string('::'): + if memberPointer: + self.fail("Expected '::' in pointer to member (function).") + break + return ASTNestedName(names, templates, rooted) + + # ========================================================================== + + def _parse_simple_type_specifiers(self) -> ASTTrailingTypeSpecFundamental: + modifier: str | None = None + signedness: str | None = None + width: list[str] = [] + typ: str | None = None + names: list[str] = [] # the parsed sequence + + self.skip_ws() + while self.match(_simple_type_specifiers_re): + t = self.matched_text + names.append(t) + if t in ('auto', 'void', 'bool', + 'char', 'wchar_t', 'char8_t', 'char16_t', 'char32_t', + 'int', '__int64', '__int128', + 'float', 'double', + '__float80', '_Float64x', '__float128', '_Float128'): + if typ is not None: + self.fail(f"Can not have both {t} and {typ}.") + typ = t + elif t in ('signed', 'unsigned'): + if signedness is not None: + self.fail(f"Can not have both {t} and {signedness}.") + signedness = t + elif t == 'short': + if len(width) != 0: + self.fail(f"Can not have both {t} and {width[0]}.") + width.append(t) + elif t == 'long': + if len(width) != 0 and width[0] != 'long': + self.fail(f"Can not have both {t} and {width[0]}.") + width.append(t) + elif t in ('_Imaginary', '_Complex'): + if modifier is not None: + self.fail(f"Can not have both {t} and {modifier}.") + modifier = t + self.skip_ws() + if len(names) == 0: + return None + + if typ in ('auto', 'void', 'bool', + 'wchar_t', 'char8_t', 'char16_t', 'char32_t', + '__float80', '_Float64x', '__float128', '_Float128'): + if modifier is not None: + self.fail(f"Can not have both {typ} and {modifier}.") + if signedness is not None: + self.fail(f"Can not have both {typ} and {signedness}.") + if len(width) != 0: + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + elif typ == 'char': + if modifier is not None: + self.fail(f"Can not have both {typ} and {modifier}.") + if len(width) != 0: + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + elif typ == 'int': + if modifier is not None: + self.fail(f"Can not have both {typ} and {modifier}.") + elif typ in ('__int64', '__int128'): + if modifier is not None: + self.fail(f"Can not have both {typ} and {modifier}.") + if len(width) != 0: + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + elif typ == 'float': + if signedness is not None: + self.fail(f"Can not have both {typ} and {signedness}.") + if len(width) != 0: + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + elif typ == 'double': + if signedness is not None: + self.fail(f"Can not have both {typ} and {signedness}.") + if len(width) > 1: + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + if len(width) == 1 and width[0] != 'long': + self.fail(f"Can not have both {typ} and {' '.join(width)}.") + elif typ is None: + if modifier is not None: + self.fail(f"Can not have {modifier} without a floating point type.") + else: + msg = f'Unhandled type {typ}' + raise AssertionError(msg) + + canonNames: list[str] = [] + if modifier is not None: + canonNames.append(modifier) + if signedness is not None: + canonNames.append(signedness) + canonNames.extend(width) + if typ is not None: + canonNames.append(typ) + return ASTTrailingTypeSpecFundamental(names, canonNames) + + def _parse_trailing_type_spec(self) -> ASTTrailingTypeSpec: + # fundamental types, https://en.cppreference.com/w/cpp/language/type + # and extensions + self.skip_ws() + res = self._parse_simple_type_specifiers() + if res is not None: + return res + + # decltype + self.skip_ws() + if self.skip_word_and_ws('decltype'): + if not self.skip_string_and_ws('('): + self.fail("Expected '(' after 'decltype'.") + if self.skip_word_and_ws('auto'): + if not self.skip_string(')'): + self.fail("Expected ')' after 'decltype(auto'.") + return ASTTrailingTypeSpecDecltypeAuto() + expr = self._parse_expression() + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' after 'decltype(<expr>'.") + return ASTTrailingTypeSpecDecltype(expr) + + # prefixed + prefix = None + self.skip_ws() + for k in ('class', 'struct', 'enum', 'union', 'typename'): + if self.skip_word_and_ws(k): + prefix = k + break + nestedName = self._parse_nested_name() + self.skip_ws() + placeholderType = None + if self.skip_word('auto'): + placeholderType = 'auto' + elif self.skip_word_and_ws('decltype'): + if not self.skip_string_and_ws('('): + self.fail("Expected '(' after 'decltype' in placeholder type specifier.") + if not self.skip_word_and_ws('auto'): + self.fail("Expected 'auto' after 'decltype(' in placeholder type specifier.") + if not self.skip_string_and_ws(')'): + self.fail("Expected ')' after 'decltype(auto' in placeholder type specifier.") + placeholderType = 'decltype(auto)' + return ASTTrailingTypeSpecName(prefix, nestedName, placeholderType) + + def _parse_parameters_and_qualifiers(self, paramMode: str) -> ASTParametersQualifiers: + if paramMode == 'new': + return None + self.skip_ws() + if not self.skip_string('('): + if paramMode == 'function': + self.fail('Expecting "(" in parameters-and-qualifiers.') + else: + return None + args = [] + self.skip_ws() + if not self.skip_string(')'): + while 1: + self.skip_ws() + if self.skip_string('...'): + args.append(ASTFunctionParameter(None, True)) + self.skip_ws() + if not self.skip_string(')'): + self.fail('Expected ")" after "..." in ' + 'parameters-and-qualifiers.') + break + # note: it seems that function arguments can always be named, + # even in function pointers and similar. + arg = self._parse_type_with_init(outer=None, named='single') + # TODO: parse default parameters # TODO: didn't we just do that? + args.append(ASTFunctionParameter(arg)) + + self.skip_ws() + if self.skip_string(','): + continue + if self.skip_string(')'): + break + self.fail('Expecting "," or ")" in parameters-and-qualifiers, ' + f'got "{self.current_char}".') + + self.skip_ws() + const = self.skip_word_and_ws('const') + volatile = self.skip_word_and_ws('volatile') + if not const: # the can be permuted + const = self.skip_word_and_ws('const') + + refQual = None + if self.skip_string('&&'): + refQual = '&&' + if not refQual and self.skip_string('&'): + refQual = '&' + + exceptionSpec = None + self.skip_ws() + if self.skip_string('noexcept'): + if self.skip_string_and_ws('('): + expr = self._parse_constant_expression(False) + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expecting ')' to end 'noexcept'.") + exceptionSpec = ASTNoexceptSpec(expr) + else: + exceptionSpec = ASTNoexceptSpec(None) + + self.skip_ws() + if self.skip_string('->'): + trailingReturn = self._parse_type(named=False) + else: + trailingReturn = None + + self.skip_ws() + override = self.skip_word_and_ws('override') + final = self.skip_word_and_ws('final') + if not override: + override = self.skip_word_and_ws( + 'override') # they can be permuted + + attrs = self._parse_attribute_list() + + self.skip_ws() + initializer = None + # if this is a function pointer we should not swallow an initializer + if paramMode == 'function' and self.skip_string('='): + self.skip_ws() + valid = ('0', 'delete', 'default') + for w in valid: + if self.skip_word_and_ws(w): + initializer = w + break + if not initializer: + self.fail( + 'Expected "%s" in initializer-specifier.' + % '" or "'.join(valid)) + + return ASTParametersQualifiers( + args, volatile, const, refQual, exceptionSpec, trailingReturn, + override, final, attrs, initializer) + + def _parse_decl_specs_simple(self, outer: str, typed: bool) -> ASTDeclSpecsSimple: + """Just parse the simple ones.""" + storage = None + threadLocal = None + inline = None + virtual = None + explicitSpec = None + consteval = None + constexpr = None + constinit = None + volatile = None + const = None + friend = None + attrs = [] + while 1: # accept any permutation of a subset of some decl-specs + self.skip_ws() + if not const and typed: + const = self.skip_word('const') + if const: + continue + if not volatile and typed: + volatile = self.skip_word('volatile') + if volatile: + continue + if not storage: + if outer in ('member', 'function'): + if self.skip_word('static'): + storage = 'static' + continue + if self.skip_word('extern'): + storage = 'extern' + continue + if outer == 'member': + if self.skip_word('mutable'): + storage = 'mutable' + continue + if self.skip_word('register'): + storage = 'register' + continue + if not inline and outer in ('function', 'member'): + inline = self.skip_word('inline') + if inline: + continue + if not constexpr and outer in ('member', 'function'): + constexpr = self.skip_word("constexpr") + if constexpr: + continue + + if outer == 'member': + if not constinit: + constinit = self.skip_word('constinit') + if constinit: + continue + if not threadLocal: + threadLocal = self.skip_word('thread_local') + if threadLocal: + continue + if outer == 'function': + if not consteval: + consteval = self.skip_word('consteval') + if consteval: + continue + if not friend: + friend = self.skip_word('friend') + if friend: + continue + if not virtual: + virtual = self.skip_word('virtual') + if virtual: + continue + if not explicitSpec: + explicit = self.skip_word_and_ws('explicit') + if explicit: + expr: ASTExpression = None + if self.skip_string('('): + expr = self._parse_constant_expression(inTemplate=False) + if not expr: + self.fail("Expected constant expression after '('" + + " in explicit specifier.") + self.skip_ws() + if not self.skip_string(')'): + self.fail("Expected ')' to end explicit specifier.") + explicitSpec = ASTExplicitSpec(expr) + continue + attr = self._parse_attribute() + if attr: + attrs.append(attr) + continue + break + return ASTDeclSpecsSimple(storage, threadLocal, inline, virtual, + explicitSpec, consteval, constexpr, constinit, + volatile, const, friend, ASTAttributeList(attrs)) + + def _parse_decl_specs(self, outer: str, typed: bool = True) -> ASTDeclSpecs: + if outer: + if outer not in ('type', 'member', 'function', 'templateParam'): + raise Exception('Internal error, unknown outer "%s".' % outer) + """ + storage-class-specifier function-specifier "constexpr" + "volatile" "const" trailing-type-specifier + + storage-class-specifier -> + "static" (only for member_object and function_object) + | "register" + + function-specifier -> "inline" | "virtual" | "explicit" (only for + function_object) + + "constexpr" (only for member_object and function_object) + """ + leftSpecs = self._parse_decl_specs_simple(outer, typed) + rightSpecs = None + + if typed: + trailing = self._parse_trailing_type_spec() + rightSpecs = self._parse_decl_specs_simple(outer, typed) + else: + trailing = None + return ASTDeclSpecs(outer, leftSpecs, rightSpecs, trailing) + + def _parse_declarator_name_suffix( + self, named: bool | str, paramMode: str, typed: bool, + ) -> ASTDeclaratorNameParamQual | ASTDeclaratorNameBitField: + # 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 self.match(identifier_re): + identifier = ASTIdentifier(self.matched_text) + nne = ASTNestedNameElement(identifier, None) + declId = ASTNestedName([nne], [False], rooted=False) + # if it's a member pointer, we may have '::', which should be an error + self.skip_ws() + if self.current_char == ':': + self.fail("Unexpected ':' after identifier.") + else: + declId = None + elif named: + declId = self._parse_nested_name() + else: + declId = None + arrayOps = [] + while 1: + self.skip_ws() + if typed and self.skip_string('['): + self.skip_ws() + if self.skip_string(']'): + arrayOps.append(ASTArray(None)) + continue + + def parser() -> ASTExpression: + return self._parse_expression() + value = self._parse_expression_fallback([']'], parser) + if not self.skip_string(']'): + self.fail("Expected ']' in end of array operator.") + arrayOps.append(ASTArray(value)) + continue + break + paramQual = self._parse_parameters_and_qualifiers(paramMode) + if paramQual is None and len(arrayOps) == 0: + # perhaps a bit-field + if named and paramMode == 'type' and typed: + self.skip_ws() + if self.skip_string(':'): + size = self._parse_constant_expression(inTemplate=False) + return ASTDeclaratorNameBitField(declId=declId, size=size) + return ASTDeclaratorNameParamQual(declId=declId, arrayOps=arrayOps, + paramQual=paramQual) + + def _parse_declarator(self, named: bool | str, paramMode: str, + typed: bool = True, + ) -> ASTDeclarator: + # 'typed' here means 'parse return type stuff' + if paramMode not in ('type', 'function', 'operatorCast', 'new'): + raise Exception( + "Internal error, unknown paramMode '%s'." % paramMode) + prevErrors = [] + self.skip_ws() + if typed and self.skip_string('*'): + self.skip_ws() + volatile = False + const = False + attrList = [] + while 1: + if not volatile: + volatile = self.skip_word_and_ws('volatile') + if volatile: + continue + if not const: + const = self.skip_word_and_ws('const') + if const: + continue + attr = self._parse_attribute() + if attr is not None: + attrList.append(attr) + continue + break + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorPtr(next=next, volatile=volatile, const=const, + attrs=ASTAttributeList(attrList)) + # TODO: shouldn't we parse an R-value ref here first? + if typed and self.skip_string("&"): + attrs = self._parse_attribute_list() + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorRef(next=next, attrs=attrs) + if typed and self.skip_string("..."): + next = self._parse_declarator(named, paramMode, False) + return ASTDeclaratorParamPack(next=next) + if typed and self.current_char == '(': # note: peeking, not skipping + if paramMode == "operatorCast": + # TODO: we should be able to parse cast operators which return + # function pointers. For now, just hax it and ignore. + return ASTDeclaratorNameParamQual(declId=None, arrayOps=[], + paramQual=None) + # maybe this is the beginning of params and quals,try that first, + # otherwise assume it's noptr->declarator > ( ptr-declarator ) + pos = self.pos + try: + # assume this is params and quals + res = self._parse_declarator_name_suffix(named, paramMode, + typed) + return res + except DefinitionError as exParamQual: + prevErrors.append((exParamQual, + "If declarator-id with parameters-and-qualifiers")) + self.pos = pos + try: + assert self.current_char == '(' + self.skip_string('(') + # TODO: hmm, if there is a name, it must be in inner, right? + # TODO: hmm, if there must be parameters, they must be + # inside, right? + inner = self._parse_declarator(named, paramMode, typed) + if not self.skip_string(')'): + self.fail("Expected ')' in \"( ptr-declarator )\"") + next = self._parse_declarator(named=False, + paramMode="type", + typed=typed) + return ASTDeclaratorParen(inner=inner, next=next) + except DefinitionError as exNoPtrParen: + self.pos = pos + prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator")) + header = "Error in declarator" + raise self._make_multi_error(prevErrors, header) from exNoPtrParen + if typed: # pointer to member + pos = self.pos + try: + name = self._parse_nested_name(memberPointer=True) + self.skip_ws() + if not self.skip_string('*'): + self.fail("Expected '*' in pointer to member declarator.") + self.skip_ws() + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If pointer to member declarator")) + else: + volatile = False + const = False + while 1: + if not volatile: + volatile = self.skip_word_and_ws('volatile') + if volatile: + continue + if not const: + const = self.skip_word_and_ws('const') + if const: + continue + break + next = self._parse_declarator(named, paramMode, typed) + return ASTDeclaratorMemPtr(name, const, volatile, next=next) + pos = self.pos + try: + res = self._parse_declarator_name_suffix(named, paramMode, typed) + # this is a heuristic for error messages, for when there is a < after a + # nested name, but it was not a successful template argument list + if self.current_char == '<': + self.otherErrors.append(self._make_multi_error(prevErrors, "")) + return res + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If declarator-id")) + header = "Error in declarator or parameters-and-qualifiers" + raise self._make_multi_error(prevErrors, header) from e + + def _parse_initializer(self, outer: str | None = None, allowFallback: bool = True, + ) -> ASTInitializer: + # initializer # global vars + # -> brace-or-equal-initializer + # | '(' expression-list ')' + # + # brace-or-equal-initializer # member vars + # -> '=' initializer-clause + # | braced-init-list + # + # initializer-clause # function params, non-type template params (with '=' in front) + # -> assignment-expression + # | braced-init-list + # + # we don't distinguish between global and member vars, so disallow paren: + # + # -> braced-init-list # var only + # | '=' assignment-expression + # | '=' braced-init-list + self.skip_ws() + if outer == 'member': + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit, hasAssign=False) + + if not self.skip_string('='): + return None + + bracedInit = self._parse_braced_init_list() + if bracedInit is not None: + return ASTInitializer(bracedInit) + + if outer == 'member': + fallbackEnd: list[str] = [] + elif outer == 'templateParam': + fallbackEnd = [',', '>'] + elif outer is None: # function parameter + fallbackEnd = [',', ')'] + else: + self.fail("Internal error, initializer for outer '%s' not " + "implemented." % outer) + + inTemplate = outer == 'templateParam' + + def parser() -> ASTExpression: + return self._parse_assignment_expression(inTemplate=inTemplate) + value = self._parse_expression_fallback(fallbackEnd, parser, allow=allowFallback) + return ASTInitializer(value) + + def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: + """ + named=False|'maybe'|True: 'maybe' is e.g., for function objects which + doesn't need to name the arguments + + outer == operatorCast: annoying case, we should not take the params + """ + if outer: # always named + if outer not in ('type', 'member', 'function', + 'operatorCast', 'templateParam'): + raise Exception('Internal error, unknown outer "%s".' % outer) + if outer != 'operatorCast': + assert named + if outer in ('type', 'function'): + # We allow type objects to just be a name. + # Some functions don't have normal return types: constructors, + # destructors, cast operators + prevErrors = [] + startPos = self.pos + # first try without the type + try: + declSpecs = self._parse_decl_specs(outer=outer, typed=False) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=False) + mustEnd = True + if outer == 'function': + # Allow trailing requires on functions. + self.skip_ws() + if re.compile(r'requires\b').match(self.definition, self.pos): + mustEnd = False + if mustEnd: + self.assert_end(allowSemicolon=True) + except DefinitionError as exUntyped: + if outer == 'type': + desc = "If just a name" + elif outer == 'function': + desc = "If the function has no return type" + else: + raise AssertionError from exUntyped + prevErrors.append((exUntyped, desc)) + self.pos = startPos + try: + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=True, paramMode=outer) + except DefinitionError as exTyped: + self.pos = startPos + if outer == 'type': + desc = "If typedef-like declaration" + elif outer == 'function': + desc = "If the function has a return type" + else: + raise AssertionError from exUntyped + prevErrors.append((exTyped, desc)) + # Retain the else branch for easier debugging. + # TODO: it would be nice to save the previous stacktrace + # and output it here. + if True: + if outer == 'type': + header = "Type must be either just a name or a " + header += "typedef-like declaration." + elif outer == 'function': + header = "Error when parsing function declaration." + else: + raise AssertionError from exUntyped + raise self._make_multi_error(prevErrors, header) from exTyped + else: # NoQA: RET506 + # For testing purposes. + # do it again to get the proper traceback (how do you + # reliably save a traceback when an exception is + # constructed?) + self.pos = startPos + typed = True + declSpecs = self._parse_decl_specs(outer=outer, typed=typed) + decl = self._parse_declarator(named=True, paramMode=outer, + typed=typed) + else: + paramMode = 'type' + if outer == 'member': + named = True + elif outer == 'operatorCast': + paramMode = 'operatorCast' + outer = None + elif outer == 'templateParam': + named = 'single' + declSpecs = self._parse_decl_specs(outer=outer) + decl = self._parse_declarator(named=named, paramMode=paramMode) + return ASTType(declSpecs, decl) + + def _parse_type_with_init( + self, named: bool | str, + outer: str) -> ASTTypeWithInit | ASTTemplateParamConstrainedTypeWithInit: + if outer: + assert outer in ('type', 'member', 'function', 'templateParam') + type = self._parse_type(outer=outer, named=named) + if outer != 'templateParam': + init = self._parse_initializer(outer=outer) + return ASTTypeWithInit(type, init) + # it could also be a constrained type parameter, e.g., C T = int& + pos = self.pos + eExpr = None + try: + init = self._parse_initializer(outer=outer, allowFallback=False) + # note: init may be None if there is no = + if init is None: + return ASTTypeWithInit(type, None) + # we parsed an expression, so we must have a , or a >, + # otherwise the expression didn't get everything + self.skip_ws() + if self.current_char != ',' and self.current_char != '>': + # pretend it didn't happen + self.pos = pos + init = None + else: + # we assume that it was indeed an expression + return ASTTypeWithInit(type, init) + except DefinitionError as e: + self.pos = pos + eExpr = e + if not self.skip_string("="): + return ASTTypeWithInit(type, None) + try: + typeInit = self._parse_type(named=False, outer=None) + return ASTTemplateParamConstrainedTypeWithInit(type, typeInit) + except DefinitionError as eType: + if eExpr is None: + raise + errs = [] + errs.append((eExpr, "If default template argument is an expression")) + errs.append((eType, "If default template argument is a type")) + msg = "Error in non-type template parameter" + msg += " or constrained template parameter." + raise self._make_multi_error(errs, msg) from eType + + def _parse_type_using(self) -> ASTTypeUsing: + name = self._parse_nested_name() + self.skip_ws() + if not self.skip_string('='): + return ASTTypeUsing(name, None) + type = self._parse_type(False, None) + return ASTTypeUsing(name, type) + + def _parse_concept(self) -> ASTConcept: + nestedName = self._parse_nested_name() + self.skip_ws() + initializer = self._parse_initializer('member') + return ASTConcept(nestedName, initializer) + + def _parse_class(self) -> ASTClass: + attrs = self._parse_attribute_list() + name = self._parse_nested_name() + self.skip_ws() + final = self.skip_word_and_ws('final') + bases = [] + self.skip_ws() + if self.skip_string(':'): + while 1: + self.skip_ws() + visibility = None + virtual = False + pack = False + if self.skip_word_and_ws('virtual'): + virtual = True + if self.match(_visibility_re): + visibility = self.matched_text + self.skip_ws() + if not virtual and self.skip_word_and_ws('virtual'): + virtual = True + baseName = self._parse_nested_name() + self.skip_ws() + pack = self.skip_string('...') + bases.append(ASTBaseClass(baseName, visibility, virtual, pack)) + self.skip_ws() + if self.skip_string(','): + continue + break + return ASTClass(name, final, bases, attrs) + + def _parse_union(self) -> ASTUnion: + attrs = self._parse_attribute_list() + name = self._parse_nested_name() + return ASTUnion(name, attrs) + + def _parse_enum(self) -> ASTEnum: + scoped = None # is set by CPPEnumObject + attrs = self._parse_attribute_list() + name = self._parse_nested_name() + self.skip_ws() + underlyingType = None + if self.skip_string(':'): + underlyingType = self._parse_type(named=False) + return ASTEnum(name, scoped, underlyingType, attrs) + + def _parse_enumerator(self) -> ASTEnumerator: + name = self._parse_nested_name() + attrs = self._parse_attribute_list() + self.skip_ws() + init = None + if self.skip_string('='): + self.skip_ws() + + def parser() -> ASTExpression: + return self._parse_constant_expression(inTemplate=False) + initVal = self._parse_expression_fallback([], parser) + init = ASTInitializer(initVal) + return ASTEnumerator(name, init, attrs) + + # ========================================================================== + + def _parse_template_parameter(self) -> ASTTemplateParam: + self.skip_ws() + if self.skip_word('template'): + # declare a template template parameter + nestedParams = self._parse_template_parameter_list() + else: + nestedParams = None + + pos = self.pos + try: + # Unconstrained type parameter or template type parameter + key = None + self.skip_ws() + if self.skip_word_and_ws('typename'): + key = 'typename' + elif self.skip_word_and_ws('class'): + key = 'class' + elif nestedParams: + self.fail("Expected 'typename' or 'class' after " + "template template parameter list.") + else: + self.fail("Expected 'typename' or 'class' in the " + "beginning of template type parameter.") + self.skip_ws() + parameterPack = self.skip_string('...') + self.skip_ws() + if self.match(identifier_re): + identifier = ASTIdentifier(self.matched_text) + else: + identifier = None + self.skip_ws() + if not parameterPack and self.skip_string('='): + default = self._parse_type(named=False, outer=None) + else: + default = None + if self.current_char not in ',>': + self.fail('Expected "," or ">" after (template) type parameter.') + data = ASTTemplateKeyParamPackIdDefault(key, identifier, + parameterPack, default) + if nestedParams: + return ASTTemplateParamTemplateType(nestedParams, data) + else: + return ASTTemplateParamType(data) + except DefinitionError as eType: + if nestedParams: + raise + try: + # non-type parameter or constrained type parameter + self.pos = pos + param = self._parse_type_with_init('maybe', 'templateParam') + self.skip_ws() + parameterPack = self.skip_string('...') + return ASTTemplateParamNonType(param, parameterPack) + except DefinitionError as eNonType: + self.pos = pos + header = "Error when parsing template parameter." + errs = [] + errs.append( + (eType, "If unconstrained type parameter or template type parameter")) + errs.append( + (eNonType, "If constrained type parameter or non-type parameter")) + raise self._make_multi_error(errs, header) from None + + def _parse_template_parameter_list(self) -> ASTTemplateParams: + # only: '<' parameter-list '>' + # we assume that 'template' has just been parsed + templateParams: list[ASTTemplateParam] = [] + self.skip_ws() + if not self.skip_string("<"): + self.fail("Expected '<' after 'template'") + while 1: + pos = self.pos + err = None + try: + param = self._parse_template_parameter() + templateParams.append(param) + except DefinitionError as eParam: + self.pos = pos + err = eParam + self.skip_ws() + if self.skip_string('>'): + requiresClause = self._parse_requires_clause() + return ASTTemplateParams(templateParams, requiresClause) + elif self.skip_string(','): + continue + else: + header = "Error in template parameter list." + errs = [] + if err: + errs.append((err, "If parameter")) + try: + self.fail('Expected "," or ">".') + except DefinitionError as e: + errs.append((e, "If no parameter")) + logger.debug(errs) + raise self._make_multi_error(errs, header) + + def _parse_template_introduction(self) -> ASTTemplateIntroduction: + pos = self.pos + try: + concept = self._parse_nested_name() + except Exception: + self.pos = pos + return None + self.skip_ws() + if not self.skip_string('{'): + self.pos = pos + return None + + # for sure it must be a template introduction now + params = [] + while 1: + self.skip_ws() + parameterPack = self.skip_string('...') + self.skip_ws() + if not self.match(identifier_re): + self.fail("Expected identifier in template introduction list.") + txt_identifier = self.matched_text + # make sure there isn't a keyword + if txt_identifier in _keywords: + self.fail("Expected identifier in template introduction list, " + "got keyword: %s" % txt_identifier) + identifier = ASTIdentifier(txt_identifier) + params.append(ASTTemplateIntroductionParameter(identifier, parameterPack)) + + self.skip_ws() + if self.skip_string('}'): + break + if self.skip_string(','): + continue + self.fail('Error in template introduction list. Expected ",", or "}".') + return ASTTemplateIntroduction(concept, params) + + def _parse_requires_clause(self) -> ASTRequiresClause | None: + # requires-clause -> 'requires' constraint-logical-or-expression + # constraint-logical-or-expression + # -> constraint-logical-and-expression + # | constraint-logical-or-expression '||' constraint-logical-and-expression + # constraint-logical-and-expression + # -> primary-expression + # | constraint-logical-and-expression '&&' primary-expression + self.skip_ws() + if not self.skip_word('requires'): + return None + + def parse_and_expr(self: DefinitionParser) -> ASTExpression: + andExprs = [] + ops = [] + andExprs.append(self._parse_primary_expression()) + while True: + self.skip_ws() + oneMore = False + if self.skip_string('&&'): + oneMore = True + ops.append('&&') + elif self.skip_word('and'): + oneMore = True + ops.append('and') + if not oneMore: + break + andExprs.append(self._parse_primary_expression()) + if len(andExprs) == 1: + return andExprs[0] + else: + return ASTBinOpExpr(andExprs, ops) + + orExprs = [] + ops = [] + orExprs.append(parse_and_expr(self)) + while True: + self.skip_ws() + oneMore = False + if self.skip_string('||'): + oneMore = True + ops.append('||') + elif self.skip_word('or'): + oneMore = True + ops.append('or') + if not oneMore: + break + orExprs.append(parse_and_expr(self)) + if len(orExprs) == 1: + return ASTRequiresClause(orExprs[0]) + else: + return ASTRequiresClause(ASTBinOpExpr(orExprs, ops)) + + def _parse_template_declaration_prefix(self, objectType: str, + ) -> ASTTemplateDeclarationPrefix | None: + templates: list[ASTTemplateParams | ASTTemplateIntroduction] = [] + while 1: + self.skip_ws() + # the saved position is only used to provide a better error message + params: ASTTemplateParams | ASTTemplateIntroduction = None + pos = self.pos + if self.skip_word("template"): + try: + params = self._parse_template_parameter_list() + except DefinitionError as e: + if objectType == 'member' and len(templates) == 0: + return ASTTemplateDeclarationPrefix(None) + else: + raise e + if objectType == 'concept' and params.requiresClause is not None: + self.fail('requires-clause not allowed for concept') + else: + params = self._parse_template_introduction() + if not params: + break + if objectType == 'concept' and len(templates) > 0: + self.pos = pos + self.fail("More than 1 template parameter list for concept.") + templates.append(params) + if len(templates) == 0 and objectType == 'concept': + self.fail('Missing template parameter list for concept.') + if len(templates) == 0: + return None + else: + return ASTTemplateDeclarationPrefix(templates) + + def _check_template_consistency(self, nestedName: ASTNestedName, + templatePrefix: ASTTemplateDeclarationPrefix, + fullSpecShorthand: bool, isMember: bool = False, + ) -> ASTTemplateDeclarationPrefix: + numArgs = nestedName.num_templates() + isMemberInstantiation = False + if not templatePrefix: + numParams = 0 + else: + if isMember and templatePrefix.templates is None: + numParams = 0 + isMemberInstantiation = True + else: + numParams = len(templatePrefix.templates) + if numArgs + 1 < numParams: + self.fail("Too few template argument lists comapred to parameter" + " lists. Argument lists: %d, Parameter lists: %d." + % (numArgs, numParams)) + if numArgs > numParams: + numExtra = numArgs - numParams + if not fullSpecShorthand and not isMemberInstantiation: + msg = "Too many template argument lists compared to parameter" \ + " lists. Argument lists: %d, Parameter lists: %d," \ + " Extra empty parameters lists prepended: %d." \ + % (numArgs, numParams, numExtra) + msg += " Declaration:\n\t" + if templatePrefix: + msg += "%s\n\t" % templatePrefix + msg += str(nestedName) + self.warn(msg) + + newTemplates: list[ASTTemplateParams | ASTTemplateIntroduction] = [] + for _i in range(numExtra): + newTemplates.append(ASTTemplateParams([], requiresClause=None)) + if templatePrefix and not isMemberInstantiation: + newTemplates.extend(templatePrefix.templates) + templatePrefix = ASTTemplateDeclarationPrefix(newTemplates) + return templatePrefix + + def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: + if objectType not in ('class', 'union', 'function', 'member', 'type', + 'concept', 'enum', 'enumerator'): + raise Exception('Internal error, unknown objectType "%s".' % objectType) + if directiveType not in ('class', 'struct', 'union', 'function', 'member', 'var', + 'type', 'concept', + 'enum', 'enum-struct', 'enum-class', 'enumerator'): + raise Exception('Internal error, unknown directiveType "%s".' % directiveType) + visibility = None + templatePrefix = None + trailingRequiresClause = None + declaration: Any = None + + self.skip_ws() + if self.match(_visibility_re): + visibility = self.matched_text + + if objectType in ('type', 'concept', 'member', 'function', 'class', 'union'): + templatePrefix = self._parse_template_declaration_prefix(objectType) + + if objectType == 'type': + prevErrors = [] + pos = self.pos + try: + if not templatePrefix: + declaration = self._parse_type(named=True, outer='type') + except DefinitionError as e: + prevErrors.append((e, "If typedef-like declaration")) + self.pos = pos + pos = self.pos + try: + if not declaration: + declaration = self._parse_type_using() + except DefinitionError as e: + self.pos = pos + prevErrors.append((e, "If type alias or template alias")) + header = "Error in type declaration." + raise self._make_multi_error(prevErrors, header) from e + elif objectType == 'concept': + declaration = self._parse_concept() + elif objectType == 'member': + declaration = self._parse_type_with_init(named=True, outer='member') + elif objectType == 'function': + declaration = self._parse_type(named=True, outer='function') + trailingRequiresClause = self._parse_requires_clause() + elif objectType == 'class': + declaration = self._parse_class() + elif objectType == 'union': + declaration = self._parse_union() + elif objectType == 'enum': + declaration = self._parse_enum() + elif objectType == 'enumerator': + declaration = self._parse_enumerator() + else: + raise AssertionError + templatePrefix = self._check_template_consistency(declaration.name, + templatePrefix, + fullSpecShorthand=False, + isMember=objectType == 'member') + self.skip_ws() + semicolon = self.skip_string(';') + return ASTDeclaration(objectType, directiveType, visibility, + templatePrefix, declaration, + trailingRequiresClause, semicolon) + + def parse_namespace_object(self) -> ASTNamespace: + templatePrefix = self._parse_template_declaration_prefix(objectType="namespace") + name = self._parse_nested_name() + templatePrefix = self._check_template_consistency(name, templatePrefix, + fullSpecShorthand=False) + res = ASTNamespace(name, templatePrefix) + res.objectType = 'namespace' # type: ignore[attr-defined] + return res + + def parse_xref_object(self) -> tuple[ASTNamespace | ASTDeclaration, bool]: + pos = self.pos + try: + templatePrefix = self._parse_template_declaration_prefix(objectType="xref") + name = self._parse_nested_name() + # if there are '()' left, just skip them + self.skip_ws() + self.skip_string('()') + self.assert_end() + templatePrefix = self._check_template_consistency(name, templatePrefix, + fullSpecShorthand=True) + res1 = ASTNamespace(name, templatePrefix) + res1.objectType = 'xref' # type: ignore[attr-defined] + return res1, True + except DefinitionError as e1: + try: + self.pos = pos + res2 = self.parse_declaration('function', 'function') + # if there are '()' left, just skip them + self.skip_ws() + self.skip_string('()') + self.assert_end() + return res2, False + except DefinitionError as e2: + errs = [] + errs.append((e1, "If shorthand ref")) + errs.append((e2, "If full function ref")) + msg = "Error in cross-reference." + raise self._make_multi_error(errs, msg) from e2 + + def parse_expression(self) -> ASTExpression | ASTType: + pos = self.pos + try: + expr = self._parse_expression() + self.skip_ws() + self.assert_end() + return expr + except DefinitionError as exExpr: + self.pos = pos + try: + typ = self._parse_type(False) + self.skip_ws() + self.assert_end() + return typ + except DefinitionError as exType: + header = "Error when parsing (type) expression." + errs = [] + errs.append((exExpr, "If expression")) + errs.append((exType, "If type")) + raise self._make_multi_error(errs, header) from exType + + +def _make_phony_error_name() -> ASTNestedName: + nne = ASTNestedNameElement(ASTIdentifier("PhonyNameDueToError"), None) + return ASTNestedName([nne], [False], rooted=False) + + +class CPPObject(ObjectDescription[ASTDeclaration]): + """Description of a C++ language object.""" + + doc_field_types: list[Field] = [ + GroupedField('template parameter', label=_('Template Parameters'), + names=('tparam', 'template parameter'), + can_collapse=True), + ] + + option_spec: OptionSpec = { + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + 'tparam-line-spec': directives.flag, + 'single-line-parameter-list': directives.flag, + } + + def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: + assert ast.objectType == 'enumerator' + # find the parent, if it exists && is an enum + # && it's unscoped, + # then add the name to the parent scope + symbol = ast.symbol + assert symbol + assert symbol.identOrOp is not None + assert symbol.templateParams is None + assert symbol.templateArgs is None + parentSymbol = symbol.parent + assert parentSymbol + if parentSymbol.parent is None: + # TODO: we could warn, but it is somewhat equivalent to unscoped + # enums, without the enum + return # no parent + parentDecl = parentSymbol.declaration + if parentDecl is None: + # the parent is not explicitly declared + # TODO: we could warn, but it could be a style to just assume + # enumerator parents to be scoped + return + if parentDecl.objectType != 'enum': + # TODO: maybe issue a warning, enumerators in non-enums is weird, + # but it is somewhat equivalent to unscoped enums, without the enum + return + if parentDecl.directiveType != 'enum': + return + + targetSymbol = parentSymbol.parent + s = targetSymbol.find_identifier(symbol.identOrOp, matchSelf=False, recurseInAnon=True, + searchInSiblings=False) + if s is not None: + # something is already declared with that name + return + declClone = symbol.declaration.clone() + declClone.enumeratorScopedSymbol = symbol + Symbol(parent=targetSymbol, identOrOp=symbol.identOrOp, + templateParams=None, templateArgs=None, + declaration=declClone, + docname=self.env.docname, line=self.get_source_info()[1]) + + def add_target_and_index(self, ast: ASTDeclaration, sig: str, + signode: TextElement) -> None: + # general note: name must be lstrip(':')'ed, to remove "::" + ids = [] + for i in range(1, _max_id + 1): + try: + id = ast.get_id(version=i) + ids.append(id) + except NoOldIdError: + assert i < _max_id + # let's keep the newest first + ids = list(reversed(ids)) + newestId = ids[0] + assert newestId # shouldn't be None + if not re.compile(r'^[a-zA-Z0-9_]*$').match(newestId): + logger.warning('Index id generation for C++ object "%s" failed, please ' + 'report as bug (id=%s).', ast, newestId, + location=self.get_location()) + + name = ast.symbol.get_full_nested_name().get_display_string().lstrip(':') + # Add index entry, but not if it's a declaration inside a concept + isInConcept = False + s = ast.symbol.parent + while s is not None: + decl = s.declaration + s = s.parent + if decl is None: + continue + if decl.objectType == 'concept': + isInConcept = True + break + if not isInConcept and 'no-index-entry' not in self.options: + strippedName = name + for prefix in self.env.config.cpp_index_common_prefix: + if name.startswith(prefix): + strippedName = strippedName[len(prefix):] + break + indexText = self.get_index_text(strippedName) + self.indexnode['entries'].append(('single', indexText, newestId, '', None)) + + if newestId not in self.state.document.ids: + # if the name is not unique, the first one will win + names = self.env.domaindata['cpp']['names'] + if name not in names: + names[name] = ast.symbol.docname + # always add the newest id + assert newestId + signode['ids'].append(newestId) + # only add compatibility ids when there are no conflicts + for id in ids[1:]: + if not id: # is None when the element didn't exist in that version + continue + if id not in self.state.document.ids: + signode['ids'].append(id) + self.state.document.note_explicit_target(signode) + + @property + def object_type(self) -> str: + raise NotImplementedError + + @property + def display_object_type(self) -> str: + return self.object_type + + def get_index_text(self, name: str) -> str: + return _('%s (C++ %s)') % (name, self.display_object_type) + + def parse_definition(self, parser: DefinitionParser) -> ASTDeclaration: + return parser.parse_declaration(self.object_type, self.objtype) + + def describe_signature(self, signode: desc_signature, + ast: ASTDeclaration, options: dict) -> None: + ast.describe_signature(signode, 'lastIsName', self.env, options) + + def run(self) -> list[Node]: + env = self.state.document.settings.env # from ObjectDescription.run + if 'cpp:parent_symbol' not in env.temp_data: + root = env.domaindata['cpp']['root_symbol'] + env.temp_data['cpp:parent_symbol'] = root + env.ref_context['cpp:parent_key'] = root.get_lookup_key() + + # The lookup keys assume that no nested scopes exists inside overloaded functions. + # (see also #5191) + # Example: + # .. cpp:function:: void f(int) + # .. cpp:function:: void f(double) + # + # .. cpp:function:: void g() + # + # :cpp:any:`boom` + # + # So we disallow any signatures inside functions. + parentSymbol = env.temp_data['cpp:parent_symbol'] + parentDecl = parentSymbol.declaration + if parentDecl is not None and parentDecl.objectType == 'function': + msg = ("C++ declarations inside functions are not supported. " + f"Parent function: {parentSymbol.get_full_nested_name()}\n" + f"Directive name: {self.name}\nDirective arg: {self.arguments[0]}") + logger.warning(msg, location=self.get_location()) + name = _make_phony_error_name() + symbol = parentSymbol.add_name(name) + env.temp_data['cpp:last_symbol'] = symbol + return [] + # When multiple declarations are made in the same directive + # they need to know about each other to provide symbol lookup for function parameters. + # We use last_symbol to store the latest added declaration in a directive. + env.temp_data['cpp:last_symbol'] = None + return super().run() + + def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: + parentSymbol: Symbol = self.env.temp_data['cpp:parent_symbol'] + + max_len = (self.env.config.cpp_maximum_signature_line_length + or self.env.config.maximum_signature_line_length + or 0) + signode['multi_line_parameter_list'] = ( + 'single-line-parameter-list' not in self.options + and (len(sig) > max_len > 0) + ) + + parser = DefinitionParser(sig, location=signode, config=self.env.config) + try: + ast = self.parse_definition(parser) + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=signode) + # It is easier to assume some phony name than handling the error in + # the possibly inner declarations. + name = _make_phony_error_name() + symbol = parentSymbol.add_name(name) + self.env.temp_data['cpp:last_symbol'] = symbol + raise ValueError from e + + try: + symbol = parentSymbol.add_declaration( + ast, docname=self.env.docname, line=self.get_source_info()[1]) + # append the new declaration to the sibling list + assert symbol.siblingAbove is None + assert symbol.siblingBelow is None + symbol.siblingAbove = self.env.temp_data['cpp:last_symbol'] + if symbol.siblingAbove is not None: + assert symbol.siblingAbove.siblingBelow is None + symbol.siblingAbove.siblingBelow = symbol + self.env.temp_data['cpp:last_symbol'] = symbol + except _DuplicateSymbolError as e: + # Assume we are actually in the old symbol, + # instead of the newly created duplicate. + self.env.temp_data['cpp:last_symbol'] = e.symbol + msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" + "Declaration is '.. cpp:%s:: %s'.") + msg = msg % (e.symbol.docname, e.symbol.line, + self.display_object_type, sig) + logger.warning(msg, location=signode) + + if ast.objectType == 'enumerator': + self._add_enumerator_to_parent(ast) + + # note: handle_signature may be called multiple time per directive, + # if it has multiple signatures, so don't mess with the original options. + options = dict(self.options) + options['tparam-line-spec'] = 'tparam-line-spec' in self.options + self.describe_signature(signode, ast, options) + return ast + + def before_content(self) -> None: + lastSymbol: Symbol = self.env.temp_data['cpp:last_symbol'] + assert lastSymbol + self.oldParentSymbol = self.env.temp_data['cpp:parent_symbol'] + self.oldParentKey: LookupKey = self.env.ref_context['cpp:parent_key'] + self.env.temp_data['cpp:parent_symbol'] = lastSymbol + self.env.ref_context['cpp:parent_key'] = lastSymbol.get_lookup_key() + self.env.temp_data['cpp:domain_name'] = ( + *self.env.temp_data.get('cpp:domain_name', ()), + lastSymbol.identOrOp._stringify(str), + ) + + def after_content(self) -> None: + self.env.temp_data['cpp:parent_symbol'] = self.oldParentSymbol + self.env.ref_context['cpp:parent_key'] = self.oldParentKey + self.env.temp_data['cpp:domain_name'] = self.env.temp_data['cpp:domain_name'][:-1] + + def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + return tuple(s.identOrOp._stringify(str) for s in + self.env.temp_data['cpp:last_symbol'].get_full_nested_name().names) + + def _toc_entry_name(self, sig_node: desc_signature) -> str: + if not sig_node.get('_toc_parts'): + return '' + + config = self.env.app.config + objtype = sig_node.parent.get('objtype') + if config.add_function_parentheses and objtype in {'function', 'method'}: + parens = '()' + else: + parens = '' + *parents, name = sig_node['_toc_parts'] + if config.toc_object_entries_show_parents == 'domain': + return '::'.join((*self.env.temp_data.get('cpp:domain_name', ()), name + parens)) + if config.toc_object_entries_show_parents == 'hide': + return name + parens + if config.toc_object_entries_show_parents == 'all': + return '::'.join(parents + [name + parens]) + return '' + + +class CPPTypeObject(CPPObject): + object_type = 'type' + + +class CPPConceptObject(CPPObject): + object_type = 'concept' + + +class CPPMemberObject(CPPObject): + object_type = 'member' + + +class CPPFunctionObject(CPPObject): + object_type = 'function' + + doc_field_types = CPPObject.doc_field_types + [ + GroupedField('parameter', label=_('Parameters'), + names=('param', 'parameter', 'arg', 'argument'), + can_collapse=True), + GroupedField('exceptions', label=_('Throws'), rolename='expr', + names=('throws', 'throw', 'exception'), + can_collapse=True), + GroupedField('retval', label=_('Return values'), + names=('retvals', 'retval'), + can_collapse=True), + Field('returnvalue', label=_('Returns'), has_arg=False, + names=('returns', 'return')), + ] + + +class CPPClassObject(CPPObject): + object_type = 'class' + + @property + def display_object_type(self) -> str: + # the distinction between class and struct is only cosmetic + assert self.objtype in ('class', 'struct') + return self.objtype + + +class CPPUnionObject(CPPObject): + object_type = 'union' + + +class CPPEnumObject(CPPObject): + object_type = 'enum' + + +class CPPEnumeratorObject(CPPObject): + object_type = 'enumerator' + + +class CPPNamespaceObject(SphinxDirective): + """ + This directive is just to tell Sphinx that we're documenting stuff in + namespace foo. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + rootSymbol = self.env.domaindata['cpp']['root_symbol'] + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + symbol = rootSymbol + stack: list[Symbol] = [] + else: + parser = DefinitionParser(self.arguments[0], + location=self.get_location(), + config=self.config) + try: + ast = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_location()) + name = _make_phony_error_name() + ast = ASTNamespace(name, None) + symbol = rootSymbol.add_name(ast.nestedName, ast.templatePrefix) + stack = [symbol] + self.env.temp_data['cpp:parent_symbol'] = symbol + self.env.temp_data['cpp:namespace_stack'] = stack + self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() + return [] + + +class CPPNamespacePushObject(SphinxDirective): + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + return [] + parser = DefinitionParser(self.arguments[0], + location=self.get_location(), + config=self.config) + try: + ast = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_location()) + name = _make_phony_error_name() + ast = ASTNamespace(name, None) + oldParent = self.env.temp_data.get('cpp:parent_symbol', None) + if not oldParent: + oldParent = self.env.domaindata['cpp']['root_symbol'] + symbol = oldParent.add_name(ast.nestedName, ast.templatePrefix) + stack = self.env.temp_data.get('cpp:namespace_stack', []) + stack.append(symbol) + self.env.temp_data['cpp:parent_symbol'] = symbol + self.env.temp_data['cpp:namespace_stack'] = stack + self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() + return [] + + +class CPPNamespacePopObject(SphinxDirective): + has_content = False + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + stack = self.env.temp_data.get('cpp:namespace_stack', None) + if not stack or len(stack) == 0: + logger.warning("C++ namespace pop on empty stack. Defaulting to global scope.", + location=self.get_location()) + stack = [] + else: + stack.pop() + if len(stack) > 0: + symbol = stack[-1] + else: + symbol = self.env.domaindata['cpp']['root_symbol'] + self.env.temp_data['cpp:parent_symbol'] = symbol + self.env.temp_data['cpp:namespace_stack'] = stack + self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() + return [] + + +class AliasNode(nodes.Element): + def __init__(self, sig: str, aliasOptions: dict, + env: BuildEnvironment | None = None, + parentKey: LookupKey | None = None) -> None: + super().__init__() + self.sig = sig + self.aliasOptions = aliasOptions + if env is not None: + if 'cpp:parent_symbol' not in env.temp_data: + root = env.domaindata['cpp']['root_symbol'] + env.temp_data['cpp:parent_symbol'] = root + env.ref_context['cpp:parent_key'] = root.get_lookup_key() + self.parentKey = env.ref_context['cpp:parent_key'] + else: + assert parentKey is not None + self.parentKey = parentKey + + def copy(self) -> AliasNode: + return self.__class__(self.sig, self.aliasOptions, + env=None, parentKey=self.parentKey) + + +class AliasTransform(SphinxTransform): + default_priority = ReferencesResolver.default_priority - 1 + + def _render_symbol(self, s: Symbol, maxdepth: int, skipThis: bool, + aliasOptions: dict, renderOptions: dict, + document: Any) -> list[Node]: + if maxdepth == 0: + recurse = True + elif maxdepth == 1: + recurse = False + else: + maxdepth -= 1 + recurse = True + + nodes: list[Node] = [] + if not skipThis: + signode = addnodes.desc_signature('', '') + nodes.append(signode) + s.declaration.describe_signature(signode, 'markName', self.env, renderOptions) + + if recurse: + if skipThis: + childContainer: list[Node] | addnodes.desc = nodes + else: + content = addnodes.desc_content() + desc = addnodes.desc() + content.append(desc) + desc.document = document + desc['domain'] = 'cpp' + # 'desctype' is a backwards compatible attribute + desc['objtype'] = desc['desctype'] = 'alias' + desc['no-index'] = True + childContainer = desc + + for sChild in s._children: + if sChild.declaration is None: + continue + if sChild.declaration.objectType in ("templateParam", "functionParam"): + continue + childNodes = self._render_symbol( + sChild, maxdepth=maxdepth, skipThis=False, + aliasOptions=aliasOptions, renderOptions=renderOptions, + document=document) + childContainer.extend(childNodes) + + if not skipThis and len(desc.children) != 0: + nodes.append(content) + return nodes + + def apply(self, **kwargs: Any) -> None: + for node in self.document.findall(AliasNode): + sig = node.sig + parentKey = node.parentKey + try: + parser = DefinitionParser(sig, location=node, + config=self.env.config) + ast, isShorthand = parser.parse_xref_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=node) + ast, isShorthand = None, None + + if ast is None: + # could not be parsed, so stop here + signode = addnodes.desc_signature(sig, '') + signode.clear() + signode += addnodes.desc_name(sig, sig) + node.replace_self(signode) + continue + + rootSymbol: Symbol = self.env.domains['cpp'].data['root_symbol'] + parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey) + if not parentSymbol: + logger.debug("Target: %s", sig) + logger.debug("ParentKey: %s", parentKey) + logger.debug(rootSymbol.dump(1)) + assert parentSymbol # should be there + + symbols: list[Symbol] = [] + if isShorthand: + assert isinstance(ast, ASTNamespace) + ns = ast + name = ns.nestedName + if ns.templatePrefix: + templateDecls = ns.templatePrefix.templates + else: + templateDecls = [] + symbols, failReason = parentSymbol.find_name( + nestedName=name, + templateDecls=templateDecls, + typ='any', + templateShorthand=True, + matchSelf=True, recurseInAnon=True, + searchInSiblings=False) + if symbols is None: + symbols = [] + else: + assert isinstance(ast, ASTDeclaration) + decl = ast + name = decl.name + s = parentSymbol.find_declaration(decl, 'any', + templateShorthand=True, + matchSelf=True, recurseInAnon=True) + if s is not None: + symbols.append(s) + + symbols = [s for s in symbols if s.declaration is not None] + + if len(symbols) == 0: + signode = addnodes.desc_signature(sig, '') + node.append(signode) + signode.clear() + signode += addnodes.desc_name(sig, sig) + + logger.warning("Can not find C++ declaration for alias '%s'." % ast, + location=node) + node.replace_self(signode) + else: + nodes = [] + renderOptions = { + 'tparam-line-spec': False, + } + for s in symbols: + assert s.declaration is not None + res = self._render_symbol( + s, maxdepth=node.aliasOptions['maxdepth'], + skipThis=node.aliasOptions['noroot'], + aliasOptions=node.aliasOptions, + renderOptions=renderOptions, + document=node.document) + nodes.extend(res) + node.replace_self(nodes) + + +class CPPAliasObject(ObjectDescription): + option_spec: OptionSpec = { + 'maxdepth': directives.nonnegative_int, + 'noroot': directives.flag, + } + + def run(self) -> list[Node]: + """ + On purpose this doesn't call the ObjectDescription version, but is based on it. + Each alias signature may expand into multiple real signatures (an overload set). + The code is therefore based on the ObjectDescription version. + """ + if ':' in self.name: + self.domain, self.objtype = self.name.split(':', 1) + else: + self.domain, self.objtype = '', self.name + + node = addnodes.desc() + node.document = self.state.document + node['domain'] = self.domain + # 'desctype' is a backwards compatible attribute + node['objtype'] = node['desctype'] = self.objtype + + self.names: list[str] = [] + aliasOptions = { + 'maxdepth': self.options.get('maxdepth', 1), + 'noroot': 'noroot' in self.options, + } + if aliasOptions['noroot'] and aliasOptions['maxdepth'] == 1: + logger.warning("Error in C++ alias declaration." + " Requested 'noroot' but 'maxdepth' 1." + " When skipping the root declaration," + " need 'maxdepth' 0 for infinite or at least 2.", + location=self.get_location()) + signatures = self.get_signatures() + for sig in signatures: + node.append(AliasNode(sig, aliasOptions, env=self.env)) + + contentnode = addnodes.desc_content() + node.append(contentnode) + self.before_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + self.env.temp_data['object'] = None + self.after_content() + return [node] + + +class CPPXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, + title: str, target: str) -> tuple[str, str]: + refnode.attributes.update(env.ref_context) + + if not has_explicit_title: + # major hax: replace anon names via simple string manipulation. + # Can this actually fail? + title = anon_identifier_re.sub("[anonymous]", str(title)) + + if refnode['reftype'] == 'any': + # Assume the removal part of fix_parens for :any: refs. + # The addition part is done with the reference is resolved. + if not has_explicit_title and title.endswith('()'): + title = title[:-2] + if target.endswith('()'): + target = target[:-2] + # TODO: should this really be here? + if not has_explicit_title: + target = target.lstrip('~') # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[:1] == '~': + title = title[1:] + dcolon = title.rfind('::') + if dcolon != -1: + title = title[dcolon + 2:] + return title, target + + +class CPPExprRole(SphinxRole): + def __init__(self, asCode: bool) -> None: + super().__init__() + if asCode: + # render the expression as inline code + self.class_type = 'cpp-expr' + else: + # render the expression as inline text + self.class_type = 'cpp-texpr' + + def run(self) -> tuple[list[Node], list[system_message]]: + text = self.text.replace('\n', ' ') + parser = DefinitionParser(text, + location=self.get_location(), + config=self.config) + # attempt to mimic XRefRole classes, except that... + try: + ast = parser.parse_expression() + except DefinitionError as ex: + logger.warning('Unparseable C++ expression: %r\n%s', text, ex, + location=self.get_location()) + # see below + return [addnodes.desc_inline('cpp', text, text, classes=[self.class_type])], [] + parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) + if parentSymbol is None: + parentSymbol = self.env.domaindata['cpp']['root_symbol'] + # ...most if not all of these classes should really apply to the individual references, + # not the container node + signode = addnodes.desc_inline('cpp', classes=[self.class_type]) + ast.describe_signature(signode, 'markType', self.env, parentSymbol) + return [signode], [] + + +class CPPDomain(Domain): + """C++ language domain. + + There are two 'object type' attributes being used:: + + - Each object created from directives gets an assigned .objtype from ObjectDescription.run. + This is simply the directive name. + - Each declaration (see the distinction in the directives dict below) has a nested .ast of + type ASTDeclaration. That object has .objectType which corresponds to the keys in the + object_types dict below. They are the core different types of declarations in C++ that + one can document. + """ + name = 'cpp' + label = 'C++' + object_types = { + 'class': ObjType(_('class'), 'class', 'struct', 'identifier', 'type'), + 'union': ObjType(_('union'), 'union', 'identifier', 'type'), + 'function': ObjType(_('function'), 'func', 'identifier', 'type'), + 'member': ObjType(_('member'), 'member', 'var', 'identifier'), + 'type': ObjType(_('type'), 'identifier', 'type'), + 'concept': ObjType(_('concept'), 'concept', 'identifier'), + 'enum': ObjType(_('enum'), 'enum', 'identifier', 'type'), + 'enumerator': ObjType(_('enumerator'), 'enumerator', 'identifier'), + # generated object types + 'functionParam': ObjType(_('function parameter'), 'identifier', 'member', 'var'), # noqa: E501 + 'templateParam': ObjType(_('template parameter'), + 'identifier', 'class', 'struct', 'union', 'member', 'var', 'type'), # noqa: E501 + } + + directives = { + # declarations + 'class': CPPClassObject, + 'struct': CPPClassObject, + 'union': CPPUnionObject, + 'function': CPPFunctionObject, + 'member': CPPMemberObject, + 'var': CPPMemberObject, + 'type': CPPTypeObject, + 'concept': CPPConceptObject, + 'enum': CPPEnumObject, + 'enum-struct': CPPEnumObject, + 'enum-class': CPPEnumObject, + 'enumerator': CPPEnumeratorObject, + # scope control + 'namespace': CPPNamespaceObject, + 'namespace-push': CPPNamespacePushObject, + 'namespace-pop': CPPNamespacePopObject, + # other + 'alias': CPPAliasObject, + } + roles = { + 'any': CPPXRefRole(), + 'class': CPPXRefRole(), + 'struct': CPPXRefRole(), + 'union': CPPXRefRole(), + 'func': CPPXRefRole(fix_parens=True), + 'member': CPPXRefRole(), + 'var': CPPXRefRole(), + 'type': CPPXRefRole(), + 'concept': CPPXRefRole(), + 'enum': CPPXRefRole(), + 'enumerator': CPPXRefRole(), + 'expr': CPPExprRole(asCode=True), + 'texpr': CPPExprRole(asCode=False), + } + initial_data = { + 'root_symbol': Symbol(None, None, None, None, None, None, None), + 'names': {}, # full name for indexing -> docname + } + + def clear_doc(self, docname: str) -> None: + if Symbol.debug_show_tree: + logger.debug("clear_doc: %s", docname) + logger.debug("\tbefore:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tbefore end") + + rootSymbol = self.data['root_symbol'] + rootSymbol.clear_doc(docname) + + if Symbol.debug_show_tree: + logger.debug("\tafter:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tafter end") + logger.debug("clear_doc end: %s", docname) + for name, nDocname in list(self.data['names'].items()): + if nDocname == docname: + del self.data['names'][name] + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + if Symbol.debug_show_tree: + logger.debug("process_doc: %s", docname) + logger.debug(self.data['root_symbol'].dump(0)) + logger.debug("process_doc end: %s", docname) + + def process_field_xref(self, pnode: pending_xref) -> None: + pnode.attributes.update(self.env.ref_context) + + def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None: + if Symbol.debug_show_tree: + logger.debug("merge_domaindata:") + logger.debug("\tself:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tself end") + logger.debug("\tother:") + logger.debug(otherdata['root_symbol'].dump(1)) + logger.debug("\tother end") + + self.data['root_symbol'].merge_with(otherdata['root_symbol'], + docnames, self.env) + ourNames = self.data['names'] + for name, docname in otherdata['names'].items(): + if docname in docnames: + if name not in ourNames: + ourNames[name] = docname + # no need to warn on duplicates, the symbol merge already does that + if Symbol.debug_show_tree: + logger.debug("\tresult:") + logger.debug(self.data['root_symbol'].dump(1)) + logger.debug("\tresult end") + logger.debug("merge_domaindata end") + + def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, + contnode: Element) -> tuple[Element | None, str | None]: + # add parens again for those that could be functions + if typ in ('any', 'func'): + target += '()' + parser = DefinitionParser(target, location=node, config=env.config) + try: + ast, isShorthand = parser.parse_xref_object() + except DefinitionError as e: + # as arg to stop flake8 from complaining + def findWarning(e: Exception) -> tuple[str, Exception]: + if typ != 'any' and typ != 'func': + return target, e + # hax on top of the paren hax to try to get correct errors + parser2 = DefinitionParser(target[:-2], + location=node, + config=env.config) + try: + parser2.parse_xref_object() + except DefinitionError as e2: + return target[:-2], e2 + # strange, that we don't get the error now, use the original + return target, e + t, ex = findWarning(e) + logger.warning('Unparseable C++ cross-reference: %r\n%s', t, ex, + location=node) + return None, None + parentKey: LookupKey = node.get("cpp:parent_key", None) + rootSymbol = self.data['root_symbol'] + if parentKey: + parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey) + if not parentSymbol: + logger.debug("Target: %s", target) + logger.debug("ParentKey: %s", parentKey.data) + logger.debug(rootSymbol.dump(1)) + assert parentSymbol # should be there + else: + parentSymbol = rootSymbol + + if isShorthand: + assert isinstance(ast, ASTNamespace) + ns = ast + name = ns.nestedName + if ns.templatePrefix: + templateDecls = ns.templatePrefix.templates + else: + templateDecls = [] + # let's be conservative with the sibling lookup for now + searchInSiblings = (not name.rooted) and len(name.names) == 1 + symbols, failReason = parentSymbol.find_name( + name, templateDecls, typ, + templateShorthand=True, + matchSelf=True, recurseInAnon=True, + searchInSiblings=searchInSiblings) + if symbols is None: + if typ == 'identifier': + if failReason == 'templateParamInQualified': + # this is an xref we created as part of a signature, + # so don't warn for names nested in template parameters + raise NoUri(str(name), typ) + s = None + else: + # just refer to the arbitrarily first symbol + s = symbols[0] + else: + assert isinstance(ast, ASTDeclaration) + decl = ast + name = decl.name + s = parentSymbol.find_declaration(decl, typ, + templateShorthand=True, + matchSelf=True, recurseInAnon=True) + if s is None or s.declaration is None: + txtName = str(name) + if txtName.startswith('std::') or txtName == 'std': + raise NoUri(txtName, typ) + return None, None + + if typ.startswith('cpp:'): + typ = typ[4:] + declTyp = s.declaration.objectType + + def checkType() -> bool: + if typ == 'any': + return True + objtypes = self.objtypes_for_role(typ) + if objtypes: + return declTyp in objtypes + logger.debug(f"Type is {typ}, declaration type is {declTyp}") # NoQA: G004 + raise AssertionError + if not checkType(): + logger.warning("cpp:%s targets a %s (%s).", + typ, s.declaration.objectType, + s.get_full_nested_name(), + location=node) + + declaration = s.declaration + if isShorthand: + fullNestedName = s.get_full_nested_name() + displayName = fullNestedName.get_display_string().lstrip(':') + else: + displayName = decl.get_display_string() + docname = s.docname + assert docname + + # the non-identifier refs are cross-references, which should be processed: + # - fix parenthesis due to operator() and add_function_parentheses + if typ != "identifier": + title = contnode.pop(0).astext() + # If it's operator(), we need to add '()' if explicit function parens + # are requested. Then the Sphinx machinery will add another pair. + # Also, if it's an 'any' ref that resolves to a function, we need to add + # parens as well. + # However, if it's a non-shorthand function ref, for a function that + # takes no arguments, then we may need to add parens again as well. + addParen = 0 + if not node.get('refexplicit', False) and declaration.objectType == 'function': + if isShorthand: + # this is just the normal haxing for 'any' roles + if env.config.add_function_parentheses and typ == 'any': + addParen += 1 + # and now this stuff for operator() + if (env.config.add_function_parentheses and typ == 'func' and + title.endswith('operator()')): + addParen += 1 + if (typ in ('any', 'func') and + title.endswith('operator') and + displayName.endswith('operator()')): + addParen += 1 + else: + # our job here is to essentially nullify add_function_parentheses + if env.config.add_function_parentheses: + if typ == 'any' and displayName.endswith('()'): + addParen += 1 + elif typ == 'func': + if title.endswith('()') and not displayName.endswith('()'): + title = title[:-2] + else: + if displayName.endswith('()'): + addParen += 1 + if addParen > 0: + title += '()' * addParen + # and reconstruct the title again + contnode += nodes.Text(title) + res = make_refnode(builder, fromdocname, docname, + declaration.get_newest_id(), contnode, displayName, + ), declaration.objectType + return res + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + return self._resolve_xref_inner(env, fromdocname, builder, typ, + target, node, contnode)[0] + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + with logging.suppress_logging(): + retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, + 'any', target, node, contnode) + if retnode: + if objtype == 'templateParam': + return [('cpp:templateParam', retnode)] + else: + return [('cpp:' + self.role_for_objtype(objtype), retnode)] + return [] + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + rootSymbol = self.data['root_symbol'] + for symbol in rootSymbol.get_all_symbols(): + if symbol.declaration is None: + continue + assert symbol.docname + fullNestedName = symbol.get_full_nested_name() + name = str(fullNestedName).lstrip(':') + dispname = fullNestedName.get_display_string().lstrip(':') + objectType = symbol.declaration.objectType + docname = symbol.docname + newestId = symbol.declaration.get_newest_id() + yield (name, dispname, objectType, docname, newestId, 1) + + def get_full_qualified_name(self, node: Element) -> str: + target = node.get('reftarget', None) + if target is None: + return None + parentKey: LookupKey = node.get("cpp:parent_key", None) + if parentKey is None or len(parentKey.data) <= 0: + return None + + rootSymbol = self.data['root_symbol'] + parentSymbol = rootSymbol.direct_lookup(parentKey) + parentName = parentSymbol.get_full_nested_name() + return '::'.join([str(parentName), target]) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(CPPDomain) + app.add_config_value("cpp_index_common_prefix", [], 'env') + app.add_config_value("cpp_id_attributes", [], 'env') + app.add_config_value("cpp_paren_attributes", [], 'env') + app.add_config_value("cpp_maximum_signature_line_length", None, 'env', types={int, None}) + app.add_post_transform(AliasTransform) + + # debug stuff + app.add_config_value("cpp_debug_lookup", False, '') + app.add_config_value("cpp_debug_show_tree", False, '') + + def initStuff(app): + Symbol.debug_lookup = app.config.cpp_debug_lookup + Symbol.debug_show_tree = app.config.cpp_debug_show_tree + app.config.cpp_index_common_prefix.sort(reverse=True) + app.connect("builder-inited", initStuff) + + return { + 'version': 'builtin', + 'env_version': 9, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/index.py b/sphinx/domains/index.py new file mode 100644 index 0000000..c084516 --- /dev/null +++ b/sphinx/domains/index.py @@ -0,0 +1,126 @@ +"""The index domain.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.domains import Domain +from sphinx.util import logging +from sphinx.util.docutils import ReferenceRole, SphinxDirective +from sphinx.util.index_entries import split_index_msg +from sphinx.util.nodes import process_index_entry + +if TYPE_CHECKING: + from collections.abc import Iterable + + from docutils.nodes import Node, system_message + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + + +logger = logging.getLogger(__name__) + + +class IndexDomain(Domain): + """Mathematics domain.""" + name = 'index' + label = 'index' + + @property + def entries(self) -> dict[str, list[tuple[str, str, str, str, str | None]]]: + return self.data.setdefault('entries', {}) + + def clear_doc(self, docname: str) -> None: + self.entries.pop(docname, None) + + def merge_domaindata(self, docnames: Iterable[str], otherdata: dict[str, Any]) -> None: + for docname in docnames: + self.entries[docname] = otherdata['entries'][docname] + + def process_doc(self, env: BuildEnvironment, docname: str, document: Node) -> None: + """Process a document after it is read by the environment.""" + entries = self.entries.setdefault(env.docname, []) + for node in list(document.findall(addnodes.index)): + try: + for (entry_type, value, _target_id, _main, _category_key) in node['entries']: + split_index_msg(entry_type, value) + except ValueError as exc: + logger.warning(str(exc), location=node) + node.parent.remove(node) + else: + for entry in node['entries']: + entries.append(entry) + + +class IndexDirective(SphinxDirective): + """ + Directive to add entries to the index. + """ + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = { + 'name': directives.unchanged, + } + + def run(self) -> list[Node]: + arguments = self.arguments[0].split('\n') + + if 'name' in self.options: + targetname = self.options['name'] + targetnode = nodes.target('', '', names=[targetname]) + else: + targetid = 'index-%s' % self.env.new_serialno('index') + targetnode = nodes.target('', '', ids=[targetid]) + + self.state.document.note_explicit_target(targetnode) + indexnode = addnodes.index() + indexnode['entries'] = [] + indexnode['inline'] = False + self.set_source_info(indexnode) + for entry in arguments: + indexnode['entries'].extend(process_index_entry(entry, targetnode['ids'][0])) + return [indexnode, targetnode] + + +class IndexRole(ReferenceRole): + def run(self) -> tuple[list[Node], list[system_message]]: + target_id = 'index-%s' % self.env.new_serialno('index') + if self.has_explicit_title: + # if an explicit target is given, process it as a full entry + title = self.title + entries = process_index_entry(self.target, target_id) + else: + # otherwise we just create a single entry + if self.target.startswith('!'): + title = self.title[1:] + entries = [('single', self.target[1:], target_id, 'main', None)] + else: + title = self.title + entries = [('single', self.target, target_id, '', None)] + + index = addnodes.index(entries=entries) + target = nodes.target('', '', ids=[target_id]) + text = nodes.Text(title) + self.set_source_info(index) + return [index, target, text], [] + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(IndexDomain) + app.add_directive('index', IndexDirective) + app.add_role('index', IndexRole()) + + return { + 'version': 'builtin', + 'env_version': 1, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py new file mode 100644 index 0000000..75149c3 --- /dev/null +++ b/sphinx/domains/javascript.py @@ -0,0 +1,508 @@ +"""The JavaScript domain.""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Any, cast + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, ObjType +from sphinx.domains.python import _pseudo_parse_arglist +from sphinx.locale import _, __ +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.docfields import Field, GroupedField, TypedField +from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import make_id, make_refnode, nested_parse_with_titles + +if TYPE_CHECKING: + from collections.abc import Iterator + + from docutils.nodes import Element, Node + + from sphinx.addnodes import desc_signature, pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + +logger = logging.getLogger(__name__) + + +class JSObject(ObjectDescription[tuple[str, str]]): + """ + Description of a JavaScript object. + """ + #: If set to ``True`` this object is callable and a `desc_parameterlist` is + #: added + has_arguments = False + + #: If ``allow_nesting`` is ``True``, the object prefixes will be accumulated + #: based on directive nesting + allow_nesting = False + + option_spec: OptionSpec = { + 'no-index': directives.flag, + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + 'single-line-parameter-list': directives.flag, + } + + def get_display_prefix(self) -> list[Node]: + #: what is displayed right before the documentation entry + return [] + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + """Breaks down construct signatures + + Parses out prefix and argument list from construct definition. The + namespace and class will be determined by the nesting of domain + directives. + """ + sig = sig.strip() + if '(' in sig and sig[-1:] == ')': + member, arglist = sig.split('(', 1) + member = member.strip() + arglist = arglist[:-1].strip() + else: + member = sig + arglist = None + # If construct is nested, prefix the current prefix + prefix = self.env.ref_context.get('js:object', None) + mod_name = self.env.ref_context.get('js:module') + + name = member + try: + member_prefix, member_name = member.rsplit('.', 1) + except ValueError: + member_name = name + member_prefix = '' + finally: + name = member_name + if prefix and member_prefix: + prefix = '.'.join([prefix, member_prefix]) + elif prefix is None and member_prefix: + prefix = member_prefix + fullname = name + if prefix: + fullname = '.'.join([prefix, name]) + + signode['module'] = mod_name + signode['object'] = prefix + signode['fullname'] = fullname + + max_len = (self.env.config.javascript_maximum_signature_line_length + or self.env.config.maximum_signature_line_length + or 0) + multi_line_parameter_list = ( + 'single-line-parameter-list' not in self.options + and (len(sig) > max_len > 0) + ) + + display_prefix = self.get_display_prefix() + if display_prefix: + signode += addnodes.desc_annotation('', '', *display_prefix) + + actual_prefix = None + if prefix: + actual_prefix = prefix + elif mod_name: + actual_prefix = mod_name + if actual_prefix: + addName = addnodes.desc_addname('', '') + for p in actual_prefix.split('.'): + addName += addnodes.desc_sig_name(p, p) + addName += addnodes.desc_sig_punctuation('.', '.') + signode += addName + signode += addnodes.desc_name('', '', addnodes.desc_sig_name(name, name)) + if self.has_arguments: + if not arglist: + signode += addnodes.desc_parameterlist() + else: + _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) + return fullname, prefix + + def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + if 'fullname' not in sig_node: + return () + modname = sig_node.get('module') + fullname = sig_node['fullname'] + + if modname: + return (modname, *fullname.split('.')) + else: + return tuple(fullname.split('.')) + + def add_target_and_index(self, name_obj: tuple[str, str], sig: str, + signode: desc_signature) -> None: + mod_name = self.env.ref_context.get('js:module') + fullname = (mod_name + '.' if mod_name else '') + name_obj[0] + node_id = make_id(self.env, self.state.document, '', fullname) + signode['ids'].append(node_id) + self.state.document.note_explicit_target(signode) + + domain = cast(JavaScriptDomain, self.env.get_domain('js')) + domain.note_object(fullname, self.objtype, node_id, location=signode) + + if 'no-index-entry' not in self.options: + indextext = self.get_index_text(mod_name, name_obj) # type: ignore[arg-type] + if indextext: + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) + + def get_index_text(self, objectname: str, name_obj: tuple[str, str]) -> str: + name, obj = name_obj + if self.objtype == 'function': + if not obj: + return _('%s() (built-in function)') % name + return _('%s() (%s method)') % (name, obj) + elif self.objtype == 'class': + return _('%s() (class)') % name + elif self.objtype == 'data': + return _('%s (global variable or constant)') % name + elif self.objtype == 'attribute': + return _('%s (%s attribute)') % (name, obj) + return '' + + def before_content(self) -> None: + """Handle object nesting before content + + :py:class:`JSObject` represents JavaScript language constructs. For + constructs that are nestable, this method will build up a stack of the + nesting hierarchy so that it can be later de-nested correctly, in + :py:meth:`after_content`. + + For constructs that aren't nestable, the stack is bypassed, and instead + only the most recent object is tracked. This object prefix name will be + removed with :py:meth:`after_content`. + + The following keys are used in ``self.env.ref_context``: + + js:objects + Stores the object prefix history. With each nested element, we + add the object prefix to this list. When we exit that object's + nesting level, :py:meth:`after_content` is triggered and the + prefix is removed from the end of the list. + + js:object + Current object prefix. This should generally reflect the last + element in the prefix history + """ + prefix = None + if self.names: + (obj_name, obj_name_prefix) = self.names.pop() + prefix = obj_name_prefix.strip('.') if obj_name_prefix else None + if self.allow_nesting: + prefix = obj_name + if prefix: + self.env.ref_context['js:object'] = prefix + if self.allow_nesting: + objects = self.env.ref_context.setdefault('js:objects', []) + objects.append(prefix) + + def after_content(self) -> None: + """Handle object de-nesting after content + + If this class is a nestable object, removing the last nested class prefix + ends further nesting in the object. + + If this class is not a nestable object, the list of classes should not + be altered as we didn't affect the nesting levels in + :py:meth:`before_content`. + """ + objects = self.env.ref_context.setdefault('js:objects', []) + if self.allow_nesting: + with contextlib.suppress(IndexError): + objects.pop() + + self.env.ref_context['js:object'] = (objects[-1] if len(objects) > 0 + else None) + + def _toc_entry_name(self, sig_node: desc_signature) -> str: + if not sig_node.get('_toc_parts'): + return '' + + config = self.env.app.config + objtype = sig_node.parent.get('objtype') + if config.add_function_parentheses and objtype in {'function', 'method'}: + parens = '()' + else: + parens = '' + *parents, name = sig_node['_toc_parts'] + if config.toc_object_entries_show_parents == 'domain': + return sig_node.get('fullname', name) + parens + if config.toc_object_entries_show_parents == 'hide': + return name + parens + if config.toc_object_entries_show_parents == 'all': + return '.'.join(parents + [name + parens]) + return '' + + +class JSCallable(JSObject): + """Description of a JavaScript function, method or constructor.""" + has_arguments = True + + doc_field_types = [ + TypedField('arguments', label=_('Arguments'), + names=('argument', 'arg', 'parameter', 'param'), + typerolename='func', typenames=('paramtype', 'type')), + GroupedField('errors', label=_('Throws'), rolename='func', + names=('throws', ), + can_collapse=True), + Field('returnvalue', label=_('Returns'), has_arg=False, + names=('returns', 'return')), + Field('returntype', label=_('Return type'), has_arg=False, + names=('rtype',)), + ] + + +class JSConstructor(JSCallable): + """Like a callable but with a different prefix.""" + + allow_nesting = True + + def get_display_prefix(self) -> list[Node]: + return [addnodes.desc_sig_keyword('class', 'class'), + addnodes.desc_sig_space()] + + +class JSModule(SphinxDirective): + """ + Directive to mark description of a new JavaScript module. + + This directive specifies the module name that will be used by objects that + follow this directive. + + Options + ------- + + no-index + If the ``:no-index:`` option is specified, no linkable elements will be + created, and the module won't be added to the global module index. This + is useful for splitting up the module definition across multiple + sections or files. + + :param mod_name: Module name + """ + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = { + 'no-index': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'nocontentsentry': directives.flag, + } + + def run(self) -> list[Node]: + mod_name = self.arguments[0].strip() + self.env.ref_context['js:module'] = mod_name + no_index = 'no-index' in self.options or 'noindex' in self.options + + content_node: Element = nodes.section() + # necessary so that the child nodes get the right source/line set + content_node.document = self.state.document + nested_parse_with_titles(self.state, self.content, content_node, self.content_offset) + + ret: list[Node] = [] + if not no_index: + domain = cast(JavaScriptDomain, self.env.get_domain('js')) + + node_id = make_id(self.env, self.state.document, 'module', mod_name) + domain.note_module(mod_name, node_id) + # Make a duplicate entry in 'objects' to facilitate searching for + # the module in JavaScriptDomain.find_obj() + domain.note_object(mod_name, 'module', node_id, + location=(self.env.docname, self.lineno)) + + # The node order is: index node first, then target node + indextext = _('%s (module)') % mod_name + inode = addnodes.index(entries=[('single', indextext, node_id, '', None)]) + ret.append(inode) + target = nodes.target('', '', ids=[node_id], ismod=True) + self.state.document.note_explicit_target(target) + ret.append(target) + ret.extend(content_node.children) + return ret + + +class JSXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, + has_explicit_title: bool, title: str, target: str) -> tuple[str, str]: + # basically what sphinx.domains.python.PyXRefRole does + refnode['js:object'] = env.ref_context.get('js:object') + refnode['js:module'] = env.ref_context.get('js:module') + if not has_explicit_title: + title = title.lstrip('.') + target = target.lstrip('~') + if title[0:1] == '~': + title = title[1:] + dot = title.rfind('.') + if dot != -1: + title = title[dot + 1:] + if target[0:1] == '.': + target = target[1:] + refnode['refspecific'] = True + return title, target + + +class JavaScriptDomain(Domain): + """JavaScript language domain.""" + name = 'js' + label = 'JavaScript' + # if you add a new object type make sure to edit JSObject.get_index_string + object_types = { + 'function': ObjType(_('function'), 'func'), + 'method': ObjType(_('method'), 'meth'), + 'class': ObjType(_('class'), 'class'), + 'data': ObjType(_('data'), 'data'), + 'attribute': ObjType(_('attribute'), 'attr'), + 'module': ObjType(_('module'), 'mod'), + } + directives = { + 'function': JSCallable, + 'method': JSCallable, + 'class': JSConstructor, + 'data': JSObject, + 'attribute': JSObject, + 'module': JSModule, + } + roles = { + 'func': JSXRefRole(fix_parens=True), + 'meth': JSXRefRole(fix_parens=True), + 'class': JSXRefRole(fix_parens=True), + 'data': JSXRefRole(), + 'attr': JSXRefRole(), + 'mod': JSXRefRole(), + } + initial_data: dict[str, dict[str, tuple[str, str]]] = { + 'objects': {}, # fullname -> docname, node_id, objtype + 'modules': {}, # modname -> docname, node_id + } + + @property + def objects(self) -> dict[str, tuple[str, str, str]]: + return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype + + def note_object(self, fullname: str, objtype: str, node_id: str, + location: Any = None) -> None: + if fullname in self.objects: + docname = self.objects[fullname][0] + logger.warning(__('duplicate %s description of %s, other %s in %s'), + objtype, fullname, objtype, docname, location=location) + self.objects[fullname] = (self.env.docname, node_id, objtype) + + @property + def modules(self) -> dict[str, tuple[str, str]]: + return self.data.setdefault('modules', {}) # modname -> docname, node_id + + def note_module(self, modname: str, node_id: str) -> None: + self.modules[modname] = (self.env.docname, node_id) + + def clear_doc(self, docname: str) -> None: + for fullname, (pkg_docname, _node_id, _l) in list(self.objects.items()): + if pkg_docname == docname: + del self.objects[fullname] + for modname, (pkg_docname, _node_id) in list(self.modules.items()): + if pkg_docname == docname: + del self.modules[modname] + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX check duplicates + for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): + if fn in docnames: + self.objects[fullname] = (fn, node_id, objtype) + for mod_name, (pkg_docname, node_id) in otherdata['modules'].items(): + if pkg_docname in docnames: + self.modules[mod_name] = (pkg_docname, node_id) + + def find_obj( + self, + env: BuildEnvironment, + mod_name: str, + prefix: str, + name: str, + typ: str | None, + searchorder: int = 0, + ) -> tuple[str | None, tuple[str, str, str] | None]: + if name[-2:] == '()': + name = name[:-2] + + searches = [] + if mod_name and prefix: + searches.append('.'.join([mod_name, prefix, name])) + if mod_name: + searches.append('.'.join([mod_name, name])) + if prefix: + searches.append('.'.join([prefix, name])) + searches.append(name) + + if searchorder == 0: + searches.reverse() + + newname = None + object_ = None + for search_name in searches: + if search_name in self.objects: + newname = search_name + object_ = self.objects[search_name] + + return newname, object_ + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + mod_name = node.get('js:module') + prefix = node.get('js:object') + searchorder = 1 if node.hasattr('refspecific') else 0 + name, obj = self.find_obj(env, mod_name, prefix, target, typ, searchorder) + if not obj: + return None + return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name) + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + mod_name = node.get('js:module') + prefix = node.get('js:object') + name, obj = self.find_obj(env, mod_name, prefix, target, None, 1) + if not obj: + return [] + return [('js:' + self.role_for_objtype(obj[2]), # type: ignore[operator] + make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))] + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + for refname, (docname, node_id, typ) in list(self.objects.items()): + yield refname, refname, typ, docname, node_id, 1 + + def get_full_qualified_name(self, node: Element) -> str | None: + modname = node.get('js:module') + prefix = node.get('js:object') + target = node.get('reftarget') + if target is None: + return None + else: + return '.'.join(filter(None, [modname, prefix, target])) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(JavaScriptDomain) + app.add_config_value( + 'javascript_maximum_signature_line_length', None, 'env', types={int, None}, + ) + return { + 'version': 'builtin', + 'env_version': 3, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py new file mode 100644 index 0000000..d283d3f --- /dev/null +++ b/sphinx/domains/math.py @@ -0,0 +1,152 @@ +"""The math domain.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from docutils import nodes +from docutils.nodes import Element, Node, make_id, system_message + +from sphinx.domains import Domain +from sphinx.locale import __ +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.nodes import make_refnode + +if TYPE_CHECKING: + from collections.abc import Iterable + + from sphinx.addnodes import pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + + +logger = logging.getLogger(__name__) + + +class MathReferenceRole(XRefRole): + def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: Element, + is_ref: bool) -> tuple[list[Node], list[system_message]]: + node['refdomain'] = 'math' + return [node], [] + + +class MathDomain(Domain): + """Mathematics domain.""" + name = 'math' + label = 'mathematics' + + initial_data: dict[str, Any] = { + 'objects': {}, # labelid -> (docname, eqno) + 'has_equations': {}, # docname -> bool + } + dangling_warnings = { + 'eq': 'equation not found: %(target)s', + } + enumerable_nodes = { # node_class -> (figtype, title_getter) + nodes.math_block: ('displaymath', None), + } + roles = { + 'numref': MathReferenceRole(), + } + + @property + def equations(self) -> dict[str, tuple[str, int]]: + return self.data.setdefault('objects', {}) # labelid -> (docname, eqno) + + def note_equation(self, docname: str, labelid: str, location: Any = None) -> None: + if labelid in self.equations: + other = self.equations[labelid][0] + logger.warning(__('duplicate label of equation %s, other instance in %s') % + (labelid, other), location=location) + + self.equations[labelid] = (docname, self.env.new_serialno('eqno') + 1) + + def get_equation_number_for(self, labelid: str) -> int | None: + if labelid in self.equations: + return self.equations[labelid][1] + else: + return None + + def process_doc(self, env: BuildEnvironment, docname: str, + document: nodes.document) -> None: + def math_node(node: Node) -> bool: + return isinstance(node, (nodes.math, nodes.math_block)) + + self.data['has_equations'][docname] = any(document.findall(math_node)) + + def clear_doc(self, docname: str) -> None: + for equation_id, (doc, _eqno) in list(self.equations.items()): + if doc == docname: + del self.equations[equation_id] + + self.data['has_equations'].pop(docname, None) + + def merge_domaindata(self, docnames: Iterable[str], otherdata: dict[str, Any]) -> None: + for labelid, (doc, eqno) in otherdata['objects'].items(): + if doc in docnames: + self.equations[labelid] = (doc, eqno) + + for docname in docnames: + self.data['has_equations'][docname] = otherdata['has_equations'][docname] + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + assert typ in ('eq', 'numref') + result = self.equations.get(target) + if result: + docname, number = result + # TODO: perhaps use rather a sphinx-core provided prefix here? + node_id = make_id('equation-%s' % target) + if env.config.math_numfig and env.config.numfig: + if docname in env.toc_fignumbers: + numbers = env.toc_fignumbers[docname]['displaymath'].get(node_id, ()) + eqno = '.'.join(map(str, numbers)) + else: + eqno = '' + else: + eqno = str(number) + + try: + eqref_format = env.config.math_eqref_format or "({number})" + title = nodes.Text(eqref_format.format(number=eqno)) + except KeyError as exc: + logger.warning(__('Invalid math_eqref_format: %r'), exc, + location=node) + title = nodes.Text("(%d)" % number) + title = nodes.Text("(%d)" % number) + return make_refnode(builder, fromdocname, docname, node_id, title) + else: + return None + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) + if refnode is None: + return [] + else: + return [('eq', refnode)] + + def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]: + return [] + + def has_equations(self, docname: str | None = None) -> bool: + if docname: + return self.data['has_equations'].get(docname, False) + else: + return any(self.data['has_equations'].values()) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(MathDomain) + app.add_role('eq', MathReferenceRole(warn_dangling=True)) + + return { + 'version': 'builtin', + 'env_version': 2, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py new file mode 100644 index 0000000..930f8e7 --- /dev/null +++ b/sphinx/domains/python.py @@ -0,0 +1,1769 @@ +"""The Python domain.""" + +from __future__ import annotations + +import ast +import builtins +import contextlib +import inspect +import re +import token +import typing +from inspect import Parameter +from typing import TYPE_CHECKING, Any, NamedTuple, cast + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, Index, IndexEntry, ObjType +from sphinx.locale import _, __ +from sphinx.pycode.parser import Token, TokenProcessor +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.docfields import Field, GroupedField, TypedField +from sphinx.util.docutils import SphinxDirective +from sphinx.util.inspect import signature_from_str +from sphinx.util.nodes import ( + find_pending_xref_condition, + make_id, + make_refnode, + nested_parse_with_titles, +) + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + from docutils.nodes import Element, Node + from docutils.parsers.rst.states import Inliner + + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec, TextlikeNode + +logger = logging.getLogger(__name__) + + +# REs for Python signatures +py_sig_re = re.compile( + r'''^ ([\w.]*\.)? # class name(s) + (\w+) \s* # thing name + (?: \[\s*(.*)\s*])? # optional: type parameters list + (?: \(\s*(.*)\s*\) # optional: arguments + (?:\s* -> \s* (.*))? # return annotation + )? $ # and nothing more + ''', re.VERBOSE) + + +pairindextypes = { + 'module': 'module', + 'keyword': 'keyword', + 'operator': 'operator', + 'object': 'object', + 'exception': 'exception', + 'statement': 'statement', + 'builtin': 'built-in function', +} + + +class ObjectEntry(NamedTuple): + docname: str + node_id: str + objtype: str + aliased: bool + + +class ModuleEntry(NamedTuple): + docname: str + node_id: str + synopsis: str + platform: str + deprecated: bool + + +def parse_reftarget(reftarget: str, suppress_prefix: bool = False, + ) -> tuple[str, str, str, bool]: + """Parse a type string and return (reftype, reftarget, title, refspecific flag)""" + refspecific = False + if reftarget.startswith('.'): + reftarget = reftarget[1:] + title = reftarget + refspecific = True + elif reftarget.startswith('~'): + reftarget = reftarget[1:] + title = reftarget.split('.')[-1] + elif suppress_prefix: + title = reftarget.split('.')[-1] + elif reftarget.startswith('typing.'): + title = reftarget[7:] + else: + title = reftarget + + if reftarget == 'None' or reftarget.startswith('typing.'): + # typing module provides non-class types. Obj reference is good to refer them. + reftype = 'obj' + else: + reftype = 'class' + + return reftype, reftarget, title, refspecific + + +def type_to_xref(target: str, env: BuildEnvironment, *, + suppress_prefix: bool = False) -> addnodes.pending_xref: + """Convert a type string to a cross reference node.""" + if env: + kwargs = {'py:module': env.ref_context.get('py:module'), + 'py:class': env.ref_context.get('py:class')} + else: + kwargs = {} + + reftype, target, title, refspecific = parse_reftarget(target, suppress_prefix) + + if env.config.python_use_unqualified_type_names: + # Note: It would be better to use qualname to describe the object to support support + # nested classes. But python domain can't access the real python object because this + # module should work not-dynamically. + shortname = title.split('.')[-1] + contnodes: list[Node] = [pending_xref_condition('', shortname, condition='resolved'), + pending_xref_condition('', title, condition='*')] + else: + contnodes = [nodes.Text(title)] + + return pending_xref('', *contnodes, + refdomain='py', reftype=reftype, reftarget=target, + refspecific=refspecific, **kwargs) + + +def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]: + """Parse type annotation.""" + short_literals = env.config.python_display_short_literal_types + + def unparse(node: ast.AST) -> list[Node]: + if isinstance(node, ast.Attribute): + return [nodes.Text(f"{unparse(node.value)[0]}.{node.attr}")] + if isinstance(node, ast.BinOp): + result: list[Node] = unparse(node.left) + result.extend(unparse(node.op)) + result.extend(unparse(node.right)) + return result + if isinstance(node, ast.BitOr): + return [addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '|'), + addnodes.desc_sig_space()] + if isinstance(node, ast.Constant): + if node.value is Ellipsis: + return [addnodes.desc_sig_punctuation('', "...")] + if isinstance(node.value, bool): + return [addnodes.desc_sig_keyword('', repr(node.value))] + if isinstance(node.value, int): + return [addnodes.desc_sig_literal_number('', repr(node.value))] + if isinstance(node.value, str): + return [addnodes.desc_sig_literal_string('', repr(node.value))] + else: + # handles None, which is further handled by type_to_xref later + # and fallback for other types that should be converted + return [nodes.Text(repr(node.value))] + if isinstance(node, ast.Expr): + return unparse(node.value) + if isinstance(node, ast.Invert): + return [addnodes.desc_sig_punctuation('', '~')] + if isinstance(node, ast.List): + result = [addnodes.desc_sig_punctuation('', '[')] + if node.elts: + # check if there are elements in node.elts to only pop the + # last element of result if the for-loop was run at least + # once + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ',')) + result.append(addnodes.desc_sig_space()) + result.pop() + result.pop() + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + if isinstance(node, ast.Module): + return sum((unparse(e) for e in node.body), []) + if isinstance(node, ast.Name): + return [nodes.Text(node.id)] + if isinstance(node, ast.Subscript): + if getattr(node.value, 'id', '') in {'Optional', 'Union'}: + return _unparse_pep_604_annotation(node) + if short_literals and getattr(node.value, 'id', '') == 'Literal': + return _unparse_pep_604_annotation(node) + result = unparse(node.value) + result.append(addnodes.desc_sig_punctuation('', '[')) + result.extend(unparse(node.slice)) + result.append(addnodes.desc_sig_punctuation('', ']')) + + # Wrap the Text nodes inside brackets by literal node if the subscript is a Literal + if result[0] in ('Literal', 'typing.Literal'): + for i, subnode in enumerate(result[1:], start=1): + if isinstance(subnode, nodes.Text): + result[i] = nodes.literal('', '', subnode) + return result + if isinstance(node, ast.UnaryOp): + return unparse(node.op) + unparse(node.operand) + if isinstance(node, ast.Tuple): + if node.elts: + result = [] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ',')) + result.append(addnodes.desc_sig_space()) + result.pop() + result.pop() + else: + result = [addnodes.desc_sig_punctuation('', '('), + addnodes.desc_sig_punctuation('', ')')] + + return result + raise SyntaxError # unsupported syntax + + def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: + subscript = node.slice + + flattened: list[Node] = [] + if isinstance(subscript, ast.Tuple): + flattened.extend(unparse(subscript.elts[0])) + for elt in subscript.elts[1:]: + flattened.extend(unparse(ast.BitOr())) + flattened.extend(unparse(elt)) + else: + # e.g. a Union[] inside an Optional[] + flattened.extend(unparse(subscript)) + + if getattr(node.value, 'id', '') == 'Optional': + flattened.extend(unparse(ast.BitOr())) + flattened.append(nodes.Text('None')) + + return flattened + + try: + tree = ast.parse(annotation, type_comments=True) + result: list[Node] = [] + for node in unparse(tree): + if isinstance(node, nodes.literal): + result.append(node[0]) + elif isinstance(node, nodes.Text) and node.strip(): + if (result and isinstance(result[-1], addnodes.desc_sig_punctuation) and + result[-1].astext() == '~'): + result.pop() + result.append(type_to_xref(str(node), env, suppress_prefix=True)) + else: + result.append(type_to_xref(str(node), env)) + else: + result.append(node) + return result + except SyntaxError: + return [type_to_xref(annotation, env)] + + +class _TypeParameterListParser(TokenProcessor): + def __init__(self, sig: str) -> None: + signature = sig.replace('\n', '').strip() + super().__init__([signature]) + # Each item is a tuple (name, kind, default, annotation) mimicking + # ``inspect.Parameter`` to allow default values on VAR_POSITIONAL + # or VAR_KEYWORD parameters. + self.type_params: list[tuple[str, int, Any, Any]] = [] + + def fetch_type_param_spec(self) -> list[Token]: + tokens = [] + while current := self.fetch_token(): + tokens.append(current) + for ldelim, rdelim in ('(', ')'), ('{', '}'), ('[', ']'): + if current == [token.OP, ldelim]: + tokens += self.fetch_until([token.OP, rdelim]) + break + else: + if current == token.INDENT: + tokens += self.fetch_until(token.DEDENT) + elif current.match( + [token.OP, ':'], [token.OP, '='], [token.OP, ',']): + tokens.pop() + break + return tokens + + def parse(self) -> None: + while current := self.fetch_token(): + if current == token.NAME: + tp_name = current.value.strip() + if self.previous and self.previous.match([token.OP, '*'], [token.OP, '**']): + if self.previous == [token.OP, '*']: + tp_kind = Parameter.VAR_POSITIONAL + else: + tp_kind = Parameter.VAR_KEYWORD # type: ignore[assignment] + else: + tp_kind = Parameter.POSITIONAL_OR_KEYWORD # type: ignore[assignment] + + tp_ann: Any = Parameter.empty + tp_default: Any = Parameter.empty + + current = self.fetch_token() + if current and current.match([token.OP, ':'], [token.OP, '=']): + if current == [token.OP, ':']: + tokens = self.fetch_type_param_spec() + tp_ann = self._build_identifier(tokens) + + if self.current and self.current == [token.OP, '=']: + tokens = self.fetch_type_param_spec() + tp_default = self._build_identifier(tokens) + + if tp_kind != Parameter.POSITIONAL_OR_KEYWORD and tp_ann != Parameter.empty: + msg = ('type parameter bound or constraint is not allowed ' + f'for {tp_kind.description} parameters') + raise SyntaxError(msg) + + type_param = (tp_name, tp_kind, tp_default, tp_ann) + self.type_params.append(type_param) + + def _build_identifier(self, tokens: list[Token]) -> str: + from itertools import chain, tee + + def pairwise(iterable): + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def triplewise(iterable): + for (a, _z), (b, c) in pairwise(pairwise(iterable)): + yield a, b, c + + idents: list[str] = [] + tokens: Iterable[Token] = iter(tokens) # type: ignore[no-redef] + # do not format opening brackets + for tok in tokens: + if not tok.match([token.OP, '('], [token.OP, '['], [token.OP, '{']): + # check if the first non-delimiter character is an unpack operator + is_unpack_operator = tok.match([token.OP, '*'], [token.OP, ['**']]) + idents.append(self._pformat_token(tok, native=is_unpack_operator)) + break + idents.append(tok.value) + + # check the remaining tokens + stop = Token(token.ENDMARKER, '', (-1, -1), (-1, -1), '<sentinel>') + is_unpack_operator = False + for tok, op, after in triplewise(chain(tokens, [stop, stop])): + ident = self._pformat_token(tok, native=is_unpack_operator) + idents.append(ident) + # determine if the next token is an unpack operator depending + # on the left and right hand side of the operator symbol + is_unpack_operator = ( + op.match([token.OP, '*'], [token.OP, '**']) and not ( + tok.match(token.NAME, token.NUMBER, token.STRING, + [token.OP, ')'], [token.OP, ']'], [token.OP, '}']) + and after.match(token.NAME, token.NUMBER, token.STRING, + [token.OP, '('], [token.OP, '['], [token.OP, '{']) + ) + ) + + return ''.join(idents).strip() + + def _pformat_token(self, tok: Token, native: bool = False) -> str: + if native: + return tok.value + + if tok.match(token.NEWLINE, token.ENDMARKER): + return '' + + if tok.match([token.OP, ':'], [token.OP, ','], [token.OP, '#']): + return f'{tok.value} ' + + # Arithmetic operators are allowed because PEP 695 specifies the + # default type parameter to be *any* expression (so "T1 << T2" is + # allowed if it makes sense). The caller is responsible to ensure + # that a multiplication operator ("*") is not to be confused with + # an unpack operator (which will not be surrounded by spaces). + # + # The operators are ordered according to how likely they are to + # be used and for (possible) future implementations (e.g., "&" for + # an intersection type). + if tok.match( + # Most likely operators to appear + [token.OP, '='], [token.OP, '|'], + # Type composition (future compatibility) + [token.OP, '&'], [token.OP, '^'], [token.OP, '<'], [token.OP, '>'], + # Unlikely type composition + [token.OP, '+'], [token.OP, '-'], [token.OP, '*'], [token.OP, '**'], + # Unlikely operators but included for completeness + [token.OP, '@'], [token.OP, '/'], [token.OP, '//'], [token.OP, '%'], + [token.OP, '<<'], [token.OP, '>>'], [token.OP, '>>>'], + [token.OP, '<='], [token.OP, '>='], [token.OP, '=='], [token.OP, '!='], + ): + return f' {tok.value} ' + + return tok.value + + +def _parse_type_list( + tp_list: str, env: BuildEnvironment, + multi_line_parameter_list: bool = False, +) -> addnodes.desc_type_parameter_list: + """Parse a list of type parameters according to PEP 695.""" + type_params = addnodes.desc_type_parameter_list(tp_list) + type_params['multi_line_parameter_list'] = multi_line_parameter_list + # formal parameter names are interpreted as type parameter names and + # type annotations are interpreted as type parameter bound or constraints + parser = _TypeParameterListParser(tp_list) + parser.parse() + for (tp_name, tp_kind, tp_default, tp_ann) in parser.type_params: + # no positional-only or keyword-only allowed in a type parameters list + if tp_kind in {Parameter.POSITIONAL_ONLY, Parameter.KEYWORD_ONLY}: + msg = ('positional-only or keyword-only parameters ' + 'are prohibited in type parameter lists') + raise SyntaxError(msg) + + node = addnodes.desc_type_parameter() + if tp_kind == Parameter.VAR_POSITIONAL: + node += addnodes.desc_sig_operator('', '*') + elif tp_kind == Parameter.VAR_KEYWORD: + node += addnodes.desc_sig_operator('', '**') + node += addnodes.desc_sig_name('', tp_name) + + if tp_ann is not Parameter.empty: + annotation = _parse_annotation(tp_ann, env) + if not annotation: + continue + + node += addnodes.desc_sig_punctuation('', ':') + node += addnodes.desc_sig_space() + + type_ann_expr = addnodes.desc_sig_name('', '', + *annotation) # type: ignore[arg-type] + # a type bound is ``T: U`` whereas type constraints + # must be enclosed with parentheses. ``T: (U, V)`` + if tp_ann.startswith('(') and tp_ann.endswith(')'): + type_ann_text = type_ann_expr.astext() + if type_ann_text.startswith('(') and type_ann_text.endswith(')'): + node += type_ann_expr + else: + # surrounding braces are lost when using _parse_annotation() + node += addnodes.desc_sig_punctuation('', '(') + node += type_ann_expr # type constraint + node += addnodes.desc_sig_punctuation('', ')') + else: + node += type_ann_expr # type bound + + if tp_default is not Parameter.empty: + # Always surround '=' with spaces, even if there is no annotation + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_operator('', '=') + node += addnodes.desc_sig_space() + node += nodes.inline('', tp_default, + classes=['default_value'], + support_smartquotes=False) + + type_params += node + return type_params + + +def _parse_arglist( + arglist: str, env: BuildEnvironment, multi_line_parameter_list: bool = False, +) -> addnodes.desc_parameterlist: + """Parse a list of arguments using AST parser""" + params = addnodes.desc_parameterlist(arglist) + params['multi_line_parameter_list'] = multi_line_parameter_list + sig = signature_from_str('(%s)' % arglist) + last_kind = None + for param in sig.parameters.values(): + if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) + if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + param.POSITIONAL_ONLY, + None): + # PEP-3102: Separator for Keyword Only Parameter: * + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*')) + + node = addnodes.desc_parameter() + if param.kind == param.VAR_POSITIONAL: + node += addnodes.desc_sig_operator('', '*') + node += addnodes.desc_sig_name('', param.name) + elif param.kind == param.VAR_KEYWORD: + node += addnodes.desc_sig_operator('', '**') + node += addnodes.desc_sig_name('', param.name) + else: + node += addnodes.desc_sig_name('', param.name) + + if param.annotation is not param.empty: + children = _parse_annotation(param.annotation, env) + node += addnodes.desc_sig_punctuation('', ':') + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_name('', '', *children) # type: ignore[arg-type] + if param.default is not param.empty: + if param.annotation is not param.empty: + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_operator('', '=') + node += addnodes.desc_sig_space() + else: + node += addnodes.desc_sig_operator('', '=') + node += nodes.inline('', param.default, classes=['default_value'], + support_smartquotes=False) + + params += node + last_kind = param.kind + + if last_kind == Parameter.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) + + return params + + +def _pseudo_parse_arglist( + signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False, +) -> None: + """"Parse" a list of arguments separated by commas. + + Arguments can have "optional" annotations given by enclosing them in + brackets. Currently, this will split at any comma, even if it's inside a + string literal (e.g. default argument value). + """ + paramlist = addnodes.desc_parameterlist() + paramlist['multi_line_parameter_list'] = multi_line_parameter_list + stack: list[Element] = [paramlist] + try: + for argument in arglist.split(','): + argument = argument.strip() + ends_open = ends_close = 0 + while argument.startswith('['): + stack.append(addnodes.desc_optional()) + stack[-2] += stack[-1] + argument = argument[1:].strip() + while argument.startswith(']'): + stack.pop() + argument = argument[1:].strip() + while argument.endswith(']') and not argument.endswith('[]'): + ends_close += 1 + argument = argument[:-1].strip() + while argument.endswith('['): + ends_open += 1 + argument = argument[:-1].strip() + if argument: + stack[-1] += addnodes.desc_parameter( + '', '', addnodes.desc_sig_name(argument, argument)) + while ends_open: + stack.append(addnodes.desc_optional()) + stack[-2] += stack[-1] + ends_open -= 1 + while ends_close: + stack.pop() + ends_close -= 1 + if len(stack) != 1: + raise IndexError + except IndexError: + # if there are too few or too many elements on the stack, just give up + # and treat the whole argument list as one argument, discarding the + # already partially populated paramlist node + paramlist = addnodes.desc_parameterlist() + paramlist += addnodes.desc_parameter(arglist, arglist) + signode += paramlist + else: + signode += paramlist + + +# This override allows our inline type specifiers to behave like :class: link +# when it comes to handling "." and "~" prefixes. +class PyXrefMixin: + def make_xref( + self, + rolename: str, + domain: str, + target: str, + innernode: type[TextlikeNode] = nodes.emphasis, + contnode: Node | None = None, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Node | None = None, + ) -> Node: + # we use inliner=None to make sure we get the old behaviour with a single + # pending_xref node + result = super().make_xref(rolename, domain, target, # type: ignore[misc] + innernode, contnode, + env, inliner=None, location=None) + if isinstance(result, pending_xref): + assert env is not None + result['refspecific'] = True + result['py:module'] = env.ref_context.get('py:module') + result['py:class'] = env.ref_context.get('py:class') + + reftype, reftarget, reftitle, _ = parse_reftarget(target) + if reftarget != reftitle: + result['reftype'] = reftype + result['reftarget'] = reftarget + + result.clear() + result += innernode(reftitle, reftitle) + elif env.config.python_use_unqualified_type_names: + children = result.children + result.clear() + + shortname = target.split('.')[-1] + textnode = innernode('', shortname) + contnodes = [pending_xref_condition('', '', textnode, condition='resolved'), + pending_xref_condition('', '', *children, condition='*')] + result.extend(contnodes) + + return result + + def make_xrefs( + self, + rolename: str, + domain: str, + target: str, + innernode: type[TextlikeNode] = nodes.emphasis, + contnode: Node | None = None, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Node | None = None, + ) -> list[Node]: + delims = r'(\s*[\[\]\(\),](?:\s*o[rf]\s)?\s*|\s+o[rf]\s+|\s*\|\s*|\.\.\.)' + delims_re = re.compile(delims) + sub_targets = re.split(delims, target) + + split_contnode = bool(contnode and contnode.astext() == target) + + in_literal = False + results = [] + for sub_target in filter(None, sub_targets): + if split_contnode: + contnode = nodes.Text(sub_target) + + if in_literal or delims_re.match(sub_target): + results.append(contnode or innernode(sub_target, sub_target)) + else: + results.append(self.make_xref(rolename, domain, sub_target, + innernode, contnode, env, inliner, location)) + + if sub_target in ('Literal', 'typing.Literal', '~typing.Literal'): + in_literal = True + + return results + + +class PyField(PyXrefMixin, Field): + pass + + +class PyGroupedField(PyXrefMixin, GroupedField): + pass + + +class PyTypedField(PyXrefMixin, TypedField): + pass + + +class PyObject(ObjectDescription[tuple[str, str]]): + """ + Description of a general Python object. + + :cvar allow_nesting: Class is an object that allows for nested namespaces + :vartype allow_nesting: bool + """ + option_spec: OptionSpec = { + 'no-index': directives.flag, + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + 'single-line-parameter-list': directives.flag, + 'single-line-type-parameter-list': directives.flag, + 'module': directives.unchanged, + 'canonical': directives.unchanged, + 'annotation': directives.unchanged, + } + + doc_field_types = [ + PyTypedField('parameter', label=_('Parameters'), + names=('param', 'parameter', 'arg', 'argument', + 'keyword', 'kwarg', 'kwparam'), + typerolename='class', typenames=('paramtype', 'type'), + can_collapse=True), + PyTypedField('variable', label=_('Variables'), + names=('var', 'ivar', 'cvar'), + typerolename='class', typenames=('vartype',), + can_collapse=True), + PyGroupedField('exceptions', label=_('Raises'), rolename='exc', + names=('raises', 'raise', 'exception', 'except'), + can_collapse=True), + Field('returnvalue', label=_('Returns'), has_arg=False, + names=('returns', 'return')), + PyField('returntype', label=_('Return type'), has_arg=False, + names=('rtype',), bodyrolename='class'), + ] + + allow_nesting = False + + def get_signature_prefix(self, sig: str) -> list[nodes.Node]: + """May return a prefix to put before the object name in the + signature. + """ + return [] + + def needs_arglist(self) -> bool: + """May return true if an empty argument list is to be generated even if + the document contains none. + """ + return False + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + """Transform a Python signature into RST nodes. + + Return (fully qualified name of the thing, classname if any). + + If inside a class, the current class name is handled intelligently: + * it is stripped from the displayed name if present + * it is added to the full name (return value) if not present + """ + m = py_sig_re.match(sig) + if m is None: + raise ValueError + prefix, name, tp_list, arglist, retann = m.groups() + + # determine module and class name (if applicable), as well as full name + modname = self.options.get('module', self.env.ref_context.get('py:module')) + classname = self.env.ref_context.get('py:class') + if classname: + add_module = False + if prefix and (prefix == classname or + prefix.startswith(classname + ".")): + fullname = prefix + name + # class name is given again in the signature + prefix = prefix[len(classname):].lstrip('.') + elif prefix: + # class name is given in the signature, but different + # (shouldn't happen) + fullname = classname + '.' + prefix + name + else: + # class name is not given in the signature + fullname = classname + '.' + name + else: + add_module = True + if prefix: + classname = prefix.rstrip('.') + fullname = prefix + name + else: + classname = '' + fullname = name + + signode['module'] = modname + signode['class'] = classname + signode['fullname'] = fullname + + max_len = (self.env.config.python_maximum_signature_line_length + or self.env.config.maximum_signature_line_length + or 0) + + # determine if the function arguments (without its type parameters) + # should be formatted on a multiline or not by removing the width of + # the type parameters list (if any) + sig_len = len(sig) + tp_list_span = m.span(3) + multi_line_parameter_list = ( + 'single-line-parameter-list' not in self.options + and (sig_len - (tp_list_span[1] - tp_list_span[0])) > max_len > 0 + ) + + # determine whether the type parameter list must be wrapped or not + arglist_span = m.span(4) + multi_line_type_parameter_list = ( + 'single-line-type-parameter-list' not in self.options + and (sig_len - (arglist_span[1] - arglist_span[0])) > max_len > 0 + ) + + sig_prefix = self.get_signature_prefix(sig) + if sig_prefix: + if type(sig_prefix) is str: + msg = ("Python directive method get_signature_prefix()" + " must return a list of nodes." + f" Return value was '{sig_prefix}'.") + raise TypeError(msg) + signode += addnodes.desc_annotation(str(sig_prefix), '', *sig_prefix) + + if prefix: + signode += addnodes.desc_addname(prefix, prefix) + elif modname and add_module and self.env.config.add_module_names: + nodetext = modname + '.' + signode += addnodes.desc_addname(nodetext, nodetext) + + signode += addnodes.desc_name(name, name) + + if tp_list: + try: + signode += _parse_type_list(tp_list, self.env, multi_line_type_parameter_list) + except Exception as exc: + logger.warning("could not parse tp_list (%r): %s", tp_list, exc, + location=signode) + + if arglist: + try: + signode += _parse_arglist(arglist, self.env, multi_line_parameter_list) + except SyntaxError: + # fallback to parse arglist original parser + # (this may happen if the argument list is incorrectly used + # as a list of bases when documenting a class) + # it supports to represent optional arguments (ex. "func(foo [, bar])") + _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) + except (NotImplementedError, ValueError) as exc: + # duplicated parameter names raise ValueError and not a SyntaxError + logger.warning("could not parse arglist (%r): %s", arglist, exc, + location=signode) + _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) + else: + if self.needs_arglist(): + # for callables, add an empty parameter list + signode += addnodes.desc_parameterlist() + + if retann: + children = _parse_annotation(retann, self.env) + signode += addnodes.desc_returns(retann, '', *children) + + anno = self.options.get('annotation') + if anno: + signode += addnodes.desc_annotation(' ' + anno, '', + addnodes.desc_sig_space(), + nodes.Text(anno)) + + return fullname, prefix + + def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + if 'fullname' not in sig_node: + return () + modname = sig_node.get('module') + fullname = sig_node['fullname'] + + if modname: + return (modname, *fullname.split('.')) + else: + return tuple(fullname.split('.')) + + def get_index_text(self, modname: str, name: tuple[str, str]) -> str: + """Return the text for the index entry of the object.""" + msg = 'must be implemented in subclasses' + raise NotImplementedError(msg) + + def add_target_and_index(self, name_cls: tuple[str, str], sig: str, + signode: desc_signature) -> None: + modname = self.options.get('module', self.env.ref_context.get('py:module')) + fullname = (modname + '.' if modname else '') + name_cls[0] + node_id = make_id(self.env, self.state.document, '', fullname) + signode['ids'].append(node_id) + self.state.document.note_explicit_target(signode) + + domain = cast(PythonDomain, self.env.get_domain('py')) + domain.note_object(fullname, self.objtype, node_id, location=signode) + + canonical_name = self.options.get('canonical') + if canonical_name: + domain.note_object(canonical_name, self.objtype, node_id, aliased=True, + location=signode) + + if 'no-index-entry' not in self.options: + indextext = self.get_index_text(modname, name_cls) + if indextext: + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) + + def before_content(self) -> None: + """Handle object nesting before content + + :py:class:`PyObject` represents Python language constructs. For + constructs that are nestable, such as a Python classes, this method will + build up a stack of the nesting hierarchy so that it can be later + de-nested correctly, in :py:meth:`after_content`. + + For constructs that aren't nestable, the stack is bypassed, and instead + only the most recent object is tracked. This object prefix name will be + removed with :py:meth:`after_content`. + """ + prefix = None + if self.names: + # fullname and name_prefix come from the `handle_signature` method. + # fullname represents the full object name that is constructed using + # object nesting and explicit prefixes. `name_prefix` is the + # explicit prefix given in a signature + (fullname, name_prefix) = self.names[-1] + if self.allow_nesting: + prefix = fullname + elif name_prefix: + prefix = name_prefix.strip('.') + if prefix: + self.env.ref_context['py:class'] = prefix + if self.allow_nesting: + classes = self.env.ref_context.setdefault('py:classes', []) + classes.append(prefix) + if 'module' in self.options: + modules = self.env.ref_context.setdefault('py:modules', []) + modules.append(self.env.ref_context.get('py:module')) + self.env.ref_context['py:module'] = self.options['module'] + + def after_content(self) -> None: + """Handle object de-nesting after content + + If this class is a nestable object, removing the last nested class prefix + ends further nesting in the object. + + If this class is not a nestable object, the list of classes should not + be altered as we didn't affect the nesting levels in + :py:meth:`before_content`. + """ + classes = self.env.ref_context.setdefault('py:classes', []) + if self.allow_nesting: + with contextlib.suppress(IndexError): + classes.pop() + + self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0 + else None) + if 'module' in self.options: + modules = self.env.ref_context.setdefault('py:modules', []) + if modules: + self.env.ref_context['py:module'] = modules.pop() + else: + self.env.ref_context.pop('py:module') + + def _toc_entry_name(self, sig_node: desc_signature) -> str: + if not sig_node.get('_toc_parts'): + return '' + + config = self.env.app.config + objtype = sig_node.parent.get('objtype') + if config.add_function_parentheses and objtype in {'function', 'method'}: + parens = '()' + else: + parens = '' + *parents, name = sig_node['_toc_parts'] + if config.toc_object_entries_show_parents == 'domain': + return sig_node.get('fullname', name) + parens + if config.toc_object_entries_show_parents == 'hide': + return name + parens + if config.toc_object_entries_show_parents == 'all': + return '.'.join(parents + [name + parens]) + return '' + + +class PyFunction(PyObject): + """Description of a function.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + option_spec.update({ + 'async': directives.flag, + }) + + def get_signature_prefix(self, sig: str) -> list[nodes.Node]: + if 'async' in self.options: + return [addnodes.desc_sig_keyword('', 'async'), + addnodes.desc_sig_space()] + else: + return [] + + def needs_arglist(self) -> bool: + return True + + def add_target_and_index(self, name_cls: tuple[str, str], sig: str, + signode: desc_signature) -> None: + super().add_target_and_index(name_cls, sig, signode) + if 'no-index-entry' not in self.options: + modname = self.options.get('module', self.env.ref_context.get('py:module')) + node_id = signode['ids'][0] + + name, cls = name_cls + if modname: + text = _('%s() (in module %s)') % (name, modname) + self.indexnode['entries'].append(('single', text, node_id, '', None)) + else: + text = f'built-in function; {name}()' + self.indexnode['entries'].append(('pair', text, node_id, '', None)) + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + # add index in own add_target_and_index() instead. + return '' + + +class PyDecoratorFunction(PyFunction): + """Description of a decorator.""" + + def run(self) -> list[Node]: + # a decorator function is a function after all + self.name = 'py:function' + return super().run() + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + ret = super().handle_signature(sig, signode) + signode.insert(0, addnodes.desc_addname('@', '@')) + return ret + + def needs_arglist(self) -> bool: + return False + + +class PyVariable(PyObject): + """Description of a variable.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + option_spec.update({ + 'type': directives.unchanged, + 'value': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + annotations = _parse_annotation(typ, self.env) + signode += addnodes.desc_annotation(typ, '', + addnodes.desc_sig_punctuation('', ':'), + addnodes.desc_sig_space(), *annotations) + + value = self.options.get('value') + if value: + signode += addnodes.desc_annotation(value, '', + addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '='), + addnodes.desc_sig_space(), + nodes.Text(value)) + + return fullname, prefix + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + name, cls = name_cls + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return _('%s (built-in variable)') % name + + +class PyClasslike(PyObject): + """ + Description of a class-like object (classes, interfaces, exceptions). + """ + + option_spec: OptionSpec = PyObject.option_spec.copy() + option_spec.update({ + 'final': directives.flag, + }) + + allow_nesting = True + + def get_signature_prefix(self, sig: str) -> list[nodes.Node]: + if 'final' in self.options: + return [nodes.Text('final'), addnodes.desc_sig_space(), + nodes.Text(self.objtype), addnodes.desc_sig_space()] + else: + return [nodes.Text(self.objtype), addnodes.desc_sig_space()] + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + if self.objtype == 'class': + if not modname: + return _('%s (built-in class)') % name_cls[0] + return _('%s (class in %s)') % (name_cls[0], modname) + elif self.objtype == 'exception': + return name_cls[0] + else: + return '' + + +class PyMethod(PyObject): + """Description of a method.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + option_spec.update({ + 'abstractmethod': directives.flag, + 'async': directives.flag, + 'classmethod': directives.flag, + 'final': directives.flag, + 'staticmethod': directives.flag, + }) + + def needs_arglist(self) -> bool: + return True + + def get_signature_prefix(self, sig: str) -> list[nodes.Node]: + prefix: list[nodes.Node] = [] + if 'final' in self.options: + prefix.append(nodes.Text('final')) + prefix.append(addnodes.desc_sig_space()) + if 'abstractmethod' in self.options: + prefix.append(nodes.Text('abstract')) + prefix.append(addnodes.desc_sig_space()) + if 'async' in self.options: + prefix.append(nodes.Text('async')) + prefix.append(addnodes.desc_sig_space()) + if 'classmethod' in self.options: + prefix.append(nodes.Text('classmethod')) + prefix.append(addnodes.desc_sig_space()) + if 'staticmethod' in self.options: + prefix.append(nodes.Text('static')) + prefix.append(addnodes.desc_sig_space()) + return prefix + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + name, cls = name_cls + try: + clsname, methname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + + if 'classmethod' in self.options: + return _('%s() (%s class method)') % (methname, clsname) + elif 'staticmethod' in self.options: + return _('%s() (%s static method)') % (methname, clsname) + else: + return _('%s() (%s method)') % (methname, clsname) + + +class PyClassMethod(PyMethod): + """Description of a classmethod.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + + def run(self) -> list[Node]: + self.name = 'py:method' + self.options['classmethod'] = True + + return super().run() + + +class PyStaticMethod(PyMethod): + """Description of a staticmethod.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + + def run(self) -> list[Node]: + self.name = 'py:method' + self.options['staticmethod'] = True + + return super().run() + + +class PyDecoratorMethod(PyMethod): + """Description of a decoratormethod.""" + + def run(self) -> list[Node]: + self.name = 'py:method' + return super().run() + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + ret = super().handle_signature(sig, signode) + signode.insert(0, addnodes.desc_addname('@', '@')) + return ret + + def needs_arglist(self) -> bool: + return False + + +class PyAttribute(PyObject): + """Description of an attribute.""" + + option_spec: OptionSpec = PyObject.option_spec.copy() + option_spec.update({ + 'type': directives.unchanged, + 'value': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + annotations = _parse_annotation(typ, self.env) + signode += addnodes.desc_annotation(typ, '', + addnodes.desc_sig_punctuation('', ':'), + addnodes.desc_sig_space(), + *annotations) + + value = self.options.get('value') + if value: + signode += addnodes.desc_annotation(value, '', + addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '='), + addnodes.desc_sig_space(), + nodes.Text(value)) + + return fullname, prefix + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + name, cls = name_cls + try: + clsname, attrname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return name + + return _('%s (%s attribute)') % (attrname, clsname) + + +class PyProperty(PyObject): + """Description of an attribute.""" + + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'abstractmethod': directives.flag, + 'classmethod': directives.flag, + 'type': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + fullname, prefix = super().handle_signature(sig, signode) + + typ = self.options.get('type') + if typ: + annotations = _parse_annotation(typ, self.env) + signode += addnodes.desc_annotation(typ, '', + addnodes.desc_sig_punctuation('', ':'), + addnodes.desc_sig_space(), + *annotations) + + return fullname, prefix + + def get_signature_prefix(self, sig: str) -> list[nodes.Node]: + prefix: list[nodes.Node] = [] + if 'abstractmethod' in self.options: + prefix.append(nodes.Text('abstract')) + prefix.append(addnodes.desc_sig_space()) + if 'classmethod' in self.options: + prefix.append(nodes.Text('class')) + prefix.append(addnodes.desc_sig_space()) + + prefix.append(nodes.Text('property')) + prefix.append(addnodes.desc_sig_space()) + return prefix + + def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str: + name, cls = name_cls + try: + clsname, attrname = name.rsplit('.', 1) + if modname and self.env.config.add_module_names: + clsname = '.'.join([modname, clsname]) + except ValueError: + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return name + + return _('%s (%s property)') % (attrname, clsname) + + +class PyModule(SphinxDirective): + """ + Directive to mark description of a new module. + """ + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = { + 'platform': lambda x: x, + 'synopsis': lambda x: x, + 'no-index': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'nocontentsentry': directives.flag, + 'deprecated': directives.flag, + } + + def run(self) -> list[Node]: + domain = cast(PythonDomain, self.env.get_domain('py')) + + modname = self.arguments[0].strip() + no_index = 'no-index' in self.options or 'noindex' in self.options + self.env.ref_context['py:module'] = modname + + content_node: Element = nodes.section() + # necessary so that the child nodes get the right source/line set + content_node.document = self.state.document + nested_parse_with_titles(self.state, self.content, content_node, self.content_offset) + + ret: list[Node] = [] + if not no_index: + # note module to the domain + node_id = make_id(self.env, self.state.document, 'module', modname) + target = nodes.target('', '', ids=[node_id], ismod=True) + self.set_source_info(target) + self.state.document.note_explicit_target(target) + + domain.note_module(modname, + node_id, + self.options.get('synopsis', ''), + self.options.get('platform', ''), + 'deprecated' in self.options) + domain.note_object(modname, 'module', node_id, location=target) + + # the platform and synopsis aren't printed; in fact, they are only + # used in the modindex currently + indextext = f'module; {modname}' + inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)]) + # The node order is: index node first, then target node. + ret.append(inode) + ret.append(target) + ret.extend(content_node.children) + return ret + + +class PyCurrentModule(SphinxDirective): + """ + This directive is just to tell Sphinx that we're documenting + stuff in module foo, but links to module foo won't lead here. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + modname = self.arguments[0].strip() + if modname == 'None': + self.env.ref_context.pop('py:module', None) + else: + self.env.ref_context['py:module'] = modname + return [] + + +class PyXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, + has_explicit_title: bool, title: str, target: str) -> tuple[str, str]: + refnode['py:module'] = env.ref_context.get('py:module') + refnode['py:class'] = env.ref_context.get('py:class') + if not has_explicit_title: + title = title.lstrip('.') # only has a meaning for the target + target = target.lstrip('~') # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[0:1] == '~': + title = title[1:] + dot = title.rfind('.') + if dot != -1: + title = title[dot + 1:] + # if the first character is a dot, search more specific namespaces first + # else search builtins first + if target[0:1] == '.': + target = target[1:] + refnode['refspecific'] = True + return title, target + + +def filter_meta_fields(app: Sphinx, domain: str, objtype: str, content: Element) -> None: + """Filter ``:meta:`` field from its docstring.""" + if domain != 'py': + return + + for node in content: + if isinstance(node, nodes.field_list): + fields = cast(list[nodes.field], node) + # removing list items while iterating the list needs reversed() + for field in reversed(fields): + field_name = cast(nodes.field_body, field[0]).astext().strip() + if field_name == 'meta' or field_name.startswith('meta '): + node.remove(field) + + +class PythonModuleIndex(Index): + """ + Index subclass to provide the Python module index. + """ + + name = 'modindex' + localname = _('Python Module Index') + shortname = _('modules') + + def generate(self, docnames: Iterable[str] | None = None, + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + content: dict[str, list[IndexEntry]] = {} + # list of prefixes to ignore + ignores: list[str] = self.domain.env.config['modindex_common_prefix'] + ignores = sorted(ignores, key=len, reverse=True) + # list of all modules, sorted by module name + modules = sorted(self.domain.data['modules'].items(), + key=lambda x: x[0].lower()) + # sort out collapsible modules + prev_modname = '' + num_toplevels = 0 + for modname, (docname, node_id, synopsis, platforms, deprecated) in modules: + if docnames and docname not in docnames: + continue + + for ignore in ignores: + if modname.startswith(ignore): + modname = modname[len(ignore):] + stripped = ignore + break + else: + stripped = '' + + # we stripped the whole module name? + if not modname: + modname, stripped = stripped, '' + + entries = content.setdefault(modname[0].lower(), []) + + package = modname.split('.')[0] + if package != modname: + # it's a submodule + if prev_modname == package: + # first submodule - make parent a group head + if entries: + last = entries[-1] + entries[-1] = IndexEntry(last[0], 1, last[2], last[3], + last[4], last[5], last[6]) + elif not prev_modname.startswith(package): + # submodule without parent in list, add dummy entry + entries.append(IndexEntry(stripped + package, 1, '', '', '', '', '')) + subtype = 2 + else: + num_toplevels += 1 + subtype = 0 + + qualifier = _('Deprecated') if deprecated else '' + entries.append(IndexEntry(stripped + modname, subtype, docname, + node_id, platforms, qualifier, synopsis)) + prev_modname = modname + + # apply heuristics when to collapse modindex at page load: + # only collapse if number of toplevel modules is larger than + # number of submodules + collapse = len(modules) - num_toplevels < num_toplevels + + # sort by first letter + sorted_content = sorted(content.items()) + + return sorted_content, collapse + + +class PythonDomain(Domain): + """Python language domain.""" + name = 'py' + label = 'Python' + object_types: dict[str, ObjType] = { + 'function': ObjType(_('function'), 'func', 'obj'), + 'data': ObjType(_('data'), 'data', 'obj'), + 'class': ObjType(_('class'), 'class', 'exc', 'obj'), + 'exception': ObjType(_('exception'), 'exc', 'class', 'obj'), + 'method': ObjType(_('method'), 'meth', 'obj'), + 'classmethod': ObjType(_('class method'), 'meth', 'obj'), + 'staticmethod': ObjType(_('static method'), 'meth', 'obj'), + 'attribute': ObjType(_('attribute'), 'attr', 'obj'), + 'property': ObjType(_('property'), 'attr', '_prop', 'obj'), + 'module': ObjType(_('module'), 'mod', 'obj'), + } + + directives = { + 'function': PyFunction, + 'data': PyVariable, + 'class': PyClasslike, + 'exception': PyClasslike, + 'method': PyMethod, + 'classmethod': PyClassMethod, + 'staticmethod': PyStaticMethod, + 'attribute': PyAttribute, + 'property': PyProperty, + 'module': PyModule, + 'currentmodule': PyCurrentModule, + 'decorator': PyDecoratorFunction, + 'decoratormethod': PyDecoratorMethod, + } + roles = { + 'data': PyXRefRole(), + 'exc': PyXRefRole(), + 'func': PyXRefRole(fix_parens=True), + 'class': PyXRefRole(), + 'const': PyXRefRole(), + 'attr': PyXRefRole(), + 'meth': PyXRefRole(fix_parens=True), + 'mod': PyXRefRole(), + 'obj': PyXRefRole(), + } + initial_data: dict[str, dict[str, tuple[Any]]] = { + 'objects': {}, # fullname -> docname, objtype + 'modules': {}, # modname -> docname, synopsis, platform, deprecated + } + indices = [ + PythonModuleIndex, + ] + + @property + def objects(self) -> dict[str, ObjectEntry]: + return self.data.setdefault('objects', {}) # fullname -> ObjectEntry + + def note_object(self, name: str, objtype: str, node_id: str, + aliased: bool = False, location: Any = None) -> None: + """Note a python object for cross reference. + + .. versionadded:: 2.1 + """ + if name in self.objects: + other = self.objects[name] + if other.aliased and aliased is False: + # The original definition found. Override it! + pass + elif other.aliased is False and aliased: + # The original definition is already registered. + return + else: + # duplicated + logger.warning(__('duplicate object description of %s, ' + 'other instance in %s, use :no-index: for one of them'), + name, other.docname, location=location) + self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype, aliased) + + @property + def modules(self) -> dict[str, ModuleEntry]: + return self.data.setdefault('modules', {}) # modname -> ModuleEntry + + def note_module(self, name: str, node_id: str, synopsis: str, + platform: str, deprecated: bool) -> None: + """Note a python module for cross reference. + + .. versionadded:: 2.1 + """ + self.modules[name] = ModuleEntry(self.env.docname, node_id, + synopsis, platform, deprecated) + + def clear_doc(self, docname: str) -> None: + for fullname, obj in list(self.objects.items()): + if obj.docname == docname: + del self.objects[fullname] + for modname, mod in list(self.modules.items()): + if mod.docname == docname: + del self.modules[modname] + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX check duplicates? + for fullname, obj in otherdata['objects'].items(): + if obj.docname in docnames: + self.objects[fullname] = obj + for modname, mod in otherdata['modules'].items(): + if mod.docname in docnames: + self.modules[modname] = mod + + def find_obj(self, env: BuildEnvironment, modname: str, classname: str, + name: str, type: str | None, searchmode: int = 0, + ) -> list[tuple[str, ObjectEntry]]: + """Find a Python object for "name", perhaps using the given module + and/or classname. Returns a list of (name, object entry) tuples. + """ + # skip parens + if name[-2:] == '()': + name = name[:-2] + + if not name: + return [] + + matches: list[tuple[str, ObjectEntry]] = [] + + newname = None + if searchmode == 1: + if type is None: + objtypes: list[str] | None = list(self.object_types) + else: + objtypes = self.objtypes_for_role(type) + if objtypes is not None: + if modname and classname: + fullname = modname + '.' + classname + '.' + name + if fullname in self.objects and self.objects[fullname].objtype in objtypes: + newname = fullname + if not newname: + if modname and modname + '.' + name in self.objects and \ + self.objects[modname + '.' + name].objtype in objtypes: + newname = modname + '.' + name + elif name in self.objects and self.objects[name].objtype in objtypes: + newname = name + else: + # "fuzzy" searching mode + searchname = '.' + name + matches = [(oname, self.objects[oname]) for oname in self.objects + if oname.endswith(searchname) and + self.objects[oname].objtype in objtypes] + else: + # NOTE: searching for exact match, object type is not considered + if name in self.objects: + newname = name + elif type == 'mod': + # only exact matches allowed for modules + return [] + elif classname and classname + '.' + name in self.objects: + newname = classname + '.' + name + elif modname and modname + '.' + name in self.objects: + newname = modname + '.' + name + elif modname and classname and \ + modname + '.' + classname + '.' + name in self.objects: + newname = modname + '.' + classname + '.' + name + if newname is not None: + matches.append((newname, self.objects[newname])) + return matches + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + type: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + modname = node.get('py:module') + clsname = node.get('py:class') + searchmode = 1 if node.hasattr('refspecific') else 0 + matches = self.find_obj(env, modname, clsname, target, + type, searchmode) + + if not matches and type == 'attr': + # fallback to meth (for property; Sphinx 2.4.x) + # this ensures that `:attr:` role continues to refer to the old property entry + # that defined by ``method`` directive in old reST files. + matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode) + if not matches and type == 'meth': + # fallback to attr (for property) + # this ensures that `:meth:` in the old reST files can refer to the property + # entry that defined by ``property`` directive. + # + # Note: _prop is a secret role only for internal look-up. + matches = self.find_obj(env, modname, clsname, target, '_prop', searchmode) + + if not matches: + return None + elif len(matches) > 1: + canonicals = [m for m in matches if not m[1].aliased] + if len(canonicals) == 1: + matches = canonicals + else: + logger.warning(__('more than one target found for cross-reference %r: %s'), + target, ', '.join(match[0] for match in matches), + type='ref', subtype='python', location=node) + name, obj = matches[0] + + if obj[2] == 'module': + return self._make_module_refnode(builder, fromdocname, name, contnode) + else: + # determine the content of the reference by conditions + content = find_pending_xref_condition(node, 'resolved') + if content: + children = content.children + else: + # if not found, use contnode + children = [contnode] + + return make_refnode(builder, fromdocname, obj[0], obj[1], children, name) + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + modname = node.get('py:module') + clsname = node.get('py:class') + results: list[tuple[str, Element]] = [] + + # always search in "refspecific" mode with the :any: role + matches = self.find_obj(env, modname, clsname, target, None, 1) + multiple_matches = len(matches) > 1 + + for name, obj in matches: + + if multiple_matches and obj.aliased: + # Skip duplicated matches + continue + + if obj[2] == 'module': + results.append(('py:mod', + self._make_module_refnode(builder, fromdocname, + name, contnode))) + else: + # determine the content of the reference by conditions + content = find_pending_xref_condition(node, 'resolved') + if content: + children = content.children + else: + # if not found, use contnode + children = [contnode] + + role = 'py:' + self.role_for_objtype(obj[2]) # type: ignore[operator] + results.append((role, make_refnode(builder, fromdocname, obj[0], obj[1], + children, name))) + return results + + def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, + contnode: Node) -> Element: + # get additional info for modules + module = self.modules[name] + title = name + if module.synopsis: + title += ': ' + module.synopsis + if module.deprecated: + title += _(' (deprecated)') + if module.platform: + title += ' (' + module.platform + ')' + return make_refnode(builder, fromdocname, module.docname, module.node_id, + contnode, title) + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + for modname, mod in self.modules.items(): + yield (modname, modname, 'module', mod.docname, mod.node_id, 0) + for refname, obj in self.objects.items(): + if obj.objtype != 'module': # modules are already handled + if obj.aliased: + # aliased names are not full-text searchable. + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, -1) + else: + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) + + def get_full_qualified_name(self, node: Element) -> str | None: + modname = node.get('py:module') + clsname = node.get('py:class') + target = node.get('reftarget') + if target is None: + return None + else: + return '.'.join(filter(None, [modname, clsname, target])) + + +def builtin_resolver(app: Sphinx, env: BuildEnvironment, + node: pending_xref, contnode: Element) -> Element | None: + """Do not emit nitpicky warnings for built-in types.""" + def istyping(s: str) -> bool: + if s.startswith('typing.'): + s = s.split('.', 1)[1] + + return s in typing.__all__ + + if node.get('refdomain') != 'py': + return None + elif node.get('reftype') in ('class', 'obj') and node.get('reftarget') == 'None': + return contnode + elif node.get('reftype') in ('class', 'obj', 'exc'): + reftarget = node.get('reftarget') + if inspect.isclass(getattr(builtins, reftarget, None)): + # built-in class + return contnode + if istyping(reftarget): + # typing class + return contnode + + return None + + +def setup(app: Sphinx) -> dict[str, Any]: + app.setup_extension('sphinx.directives') + + app.add_domain(PythonDomain) + app.add_config_value('python_use_unqualified_type_names', False, 'env') + app.add_config_value('python_maximum_signature_line_length', None, 'env', + types={int, None}) + app.add_config_value('python_display_short_literal_types', False, 'env') + app.connect('object-description-transform', filter_meta_fields) + app.connect('missing-reference', builtin_resolver, priority=900) + + return { + 'version': 'builtin', + 'env_version': 4, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py new file mode 100644 index 0000000..480aba5 --- /dev/null +++ b/sphinx/domains/rst.py @@ -0,0 +1,299 @@ +"""The reStructuredText domain.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Any, cast + +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, ObjType +from sphinx.locale import _, __ +from sphinx.roles import XRefRole +from sphinx.util import logging +from sphinx.util.nodes import make_id, make_refnode + +if TYPE_CHECKING: + from collections.abc import Iterator + + from docutils.nodes import Element + + from sphinx.addnodes import desc_signature, pending_xref + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec + +logger = logging.getLogger(__name__) + +dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') + + +class ReSTMarkup(ObjectDescription[str]): + """ + Description of generic reST markup. + """ + option_spec: OptionSpec = { + 'no-index': directives.flag, + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + } + + def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: + node_id = make_id(self.env, self.state.document, self.objtype, name) + signode['ids'].append(node_id) + self.state.document.note_explicit_target(signode) + + domain = cast(ReSTDomain, self.env.get_domain('rst')) + domain.note_object(self.objtype, name, node_id, location=signode) + + if 'no-index-entry' not in self.options: + indextext = self.get_index_text(self.objtype, name) + if indextext: + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) + + def get_index_text(self, objectname: str, name: str) -> str: + return '' + + def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + if 'fullname' not in sig_node: + return () + directive_names = [] + for parent in self.env.ref_context.get('rst:directives', ()): + directive_names += parent.split(':') + name = sig_node['fullname'] + return tuple(directive_names + name.split(':')) + + def _toc_entry_name(self, sig_node: desc_signature) -> str: + if not sig_node.get('_toc_parts'): + return '' + + config = self.env.app.config + objtype = sig_node.parent.get('objtype') + *parents, name = sig_node['_toc_parts'] + if objtype == 'directive:option': + return f':{name}:' + if config.toc_object_entries_show_parents in {'domain', 'all'}: + name = ':'.join(sig_node['_toc_parts']) + if objtype == 'role': + return f':{name}:' + if objtype == 'directive': + return f'.. {name}::' + return '' + + +def parse_directive(d: str) -> tuple[str, str]: + """Parse a directive signature. + + Returns (directive, arguments) string tuple. If no arguments are given, + returns (directive, ''). + """ + dir = d.strip() + if not dir.startswith('.'): + # Assume it is a directive without syntax + return (dir, '') + m = dir_sig_re.match(dir) + if not m: + return (dir, '') + parsed_dir, parsed_args = m.groups() + if parsed_args.strip(): + return (parsed_dir.strip(), ' ' + parsed_args.strip()) + else: + return (parsed_dir.strip(), '') + + +class ReSTDirective(ReSTMarkup): + """ + Description of a reST directive. + """ + def handle_signature(self, sig: str, signode: desc_signature) -> str: + name, args = parse_directive(sig) + desc_name = f'.. {name}::' + signode['fullname'] = name.strip() + signode += addnodes.desc_name(desc_name, desc_name) + if len(args) > 0: + signode += addnodes.desc_addname(args, args) + return name + + def get_index_text(self, objectname: str, name: str) -> str: + return _('%s (directive)') % name + + def before_content(self) -> None: + if self.names: + directives = self.env.ref_context.setdefault('rst:directives', []) + directives.append(self.names[0]) + + def after_content(self) -> None: + directives = self.env.ref_context.setdefault('rst:directives', []) + if directives: + directives.pop() + + +class ReSTDirectiveOption(ReSTMarkup): + """ + Description of an option for reST directive. + """ + option_spec: OptionSpec = ReSTMarkup.option_spec.copy() + option_spec.update({ + 'type': directives.unchanged, + }) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + try: + name, argument = re.split(r'\s*:\s+', sig.strip(), maxsplit=1) + except ValueError: + name, argument = sig, None + + desc_name = f':{name}:' + signode['fullname'] = name.strip() + signode += addnodes.desc_name(desc_name, desc_name) + if argument: + signode += addnodes.desc_annotation(' ' + argument, ' ' + argument) + if self.options.get('type'): + text = ' (%s)' % self.options['type'] + signode += addnodes.desc_annotation(text, text) + return name + + def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: + domain = cast(ReSTDomain, self.env.get_domain('rst')) + + directive_name = self.current_directive + if directive_name: + prefix = '-'.join([self.objtype, directive_name]) + objname = ':'.join([directive_name, name]) + else: + prefix = self.objtype + objname = name + + node_id = make_id(self.env, self.state.document, prefix, name) + signode['ids'].append(node_id) + self.state.document.note_explicit_target(signode) + domain.note_object(self.objtype, objname, node_id, location=signode) + + if directive_name: + key = name[0].upper() + pair = [_('%s (directive)') % directive_name, + _(':%s: (directive option)') % name] + self.indexnode['entries'].append(('pair', '; '.join(pair), node_id, '', key)) + else: + key = name[0].upper() + text = _(':%s: (directive option)') % name + self.indexnode['entries'].append(('single', text, node_id, '', key)) + + @property + def current_directive(self) -> str: + directives = self.env.ref_context.get('rst:directives') + if directives: + return directives[-1] + else: + return '' + + +class ReSTRole(ReSTMarkup): + """ + Description of a reST role. + """ + def handle_signature(self, sig: str, signode: desc_signature) -> str: + desc_name = f':{sig}:' + signode['fullname'] = sig.strip() + signode += addnodes.desc_name(desc_name, desc_name) + return sig + + def get_index_text(self, objectname: str, name: str) -> str: + return _('%s (role)') % name + + +class ReSTDomain(Domain): + """ReStructuredText domain.""" + name = 'rst' + label = 'reStructuredText' + + object_types = { + 'directive': ObjType(_('directive'), 'dir'), + 'directive:option': ObjType(_('directive-option'), 'dir'), + 'role': ObjType(_('role'), 'role'), + } + directives = { + 'directive': ReSTDirective, + 'directive:option': ReSTDirectiveOption, + 'role': ReSTRole, + } + roles = { + 'dir': XRefRole(), + 'role': XRefRole(), + } + initial_data: dict[str, dict[tuple[str, str], str]] = { + 'objects': {}, # fullname -> docname, objtype + } + + @property + def objects(self) -> dict[tuple[str, str], tuple[str, str]]: + return self.data.setdefault('objects', {}) # (objtype, fullname) -> (docname, node_id) + + def note_object(self, objtype: str, name: str, node_id: str, location: Any = None) -> None: + if (objtype, name) in self.objects: + docname, node_id = self.objects[objtype, name] + logger.warning(__('duplicate description of %s %s, other instance in %s') % + (objtype, name, docname), location=location) + + self.objects[objtype, name] = (self.env.docname, node_id) + + def clear_doc(self, docname: str) -> None: + for (typ, name), (doc, _node_id) in list(self.objects.items()): + if doc == docname: + del self.objects[typ, name] + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX check duplicates + for (typ, name), (doc, node_id) in otherdata['objects'].items(): + if doc in docnames: + self.objects[typ, name] = (doc, node_id) + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + objtypes = self.objtypes_for_role(typ) + if not objtypes: + return None + for objtype in objtypes: + result = self.objects.get((objtype, target)) + if result: + todocname, node_id = result + return make_refnode(builder, fromdocname, todocname, node_id, + contnode, target + ' ' + objtype) + return None + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + target: str, node: pending_xref, contnode: Element, + ) -> list[tuple[str, Element]]: + results: list[tuple[str, Element]] = [] + for objtype in self.object_types: + result = self.objects.get((objtype, target)) + if result: + todocname, node_id = result + results.append( + ('rst:' + self.role_for_objtype(objtype), # type: ignore[operator] + make_refnode(builder, fromdocname, todocname, node_id, + contnode, target + ' ' + objtype))) + return results + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + for (typ, name), (docname, node_id) in self.data['objects'].items(): + yield name, name, typ, docname, node_id, 1 + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(ReSTDomain) + + return { + 'version': 'builtin', + 'env_version': 2, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py new file mode 100644 index 0000000..b3082a7 --- /dev/null +++ b/sphinx/domains/std.py @@ -0,0 +1,1123 @@ +"""The standard domain.""" + +from __future__ import annotations + +import re +from copy import copy +from typing import TYPE_CHECKING, Any, Callable, Final, cast + +from docutils import nodes +from docutils.nodes import Element, Node, system_message +from docutils.parsers.rst import Directive, directives +from docutils.statemachine import StringList + +from sphinx import addnodes +from sphinx.addnodes import desc_signature, pending_xref +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, ObjType, TitleGetter +from sphinx.locale import _, __ +from sphinx.roles import EmphasizedLiteral, XRefRole +from sphinx.util import docname_join, logging, ws_re +from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import clean_astext, make_id, make_refnode + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec, RoleFunction + +logger = logging.getLogger(__name__) + +# RE for option descriptions +option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)') +# RE for grammar tokens +token_re = re.compile(r'`((~?\w*:)?\w+)`', re.U) + +samp_role = EmphasizedLiteral() + + +class GenericObject(ObjectDescription[str]): + """ + A generic x-ref directive registered with Sphinx.add_object_type(). + """ + indextemplate: str = '' + parse_node: Callable[[BuildEnvironment, str, desc_signature], str] | None = None + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + if self.parse_node: + name = self.parse_node(self.env, sig, signode) + else: + signode.clear() + signode += addnodes.desc_name(sig, sig) + # normalize whitespace like XRefRole does + name = ws_re.sub(' ', sig) + return name + + def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: + node_id = make_id(self.env, self.state.document, self.objtype, name) + signode['ids'].append(node_id) + self.state.document.note_explicit_target(signode) + + if self.indextemplate: + colon = self.indextemplate.find(':') + if colon != -1: + indextype = self.indextemplate[:colon].strip() + indexentry = self.indextemplate[colon + 1:].strip() % (name,) + else: + indextype = 'single' + indexentry = self.indextemplate % (name,) + self.indexnode['entries'].append((indextype, indexentry, node_id, '', None)) + + std = cast(StandardDomain, self.env.get_domain('std')) + std.note_object(self.objtype, name, node_id, location=signode) + + +class EnvVar(GenericObject): + indextemplate = _('environment variable; %s') + + +class EnvVarXRefRole(XRefRole): + """ + Cross-referencing role for environment variables (adds an index entry). + """ + + def result_nodes(self, document: nodes.document, env: BuildEnvironment, node: Element, + is_ref: bool) -> tuple[list[Node], list[system_message]]: + if not is_ref: + return [node], [] + varname = node['reftarget'] + tgtid = 'index-%s' % env.new_serialno('index') + indexnode = addnodes.index() + indexnode['entries'] = [ + ('single', varname, tgtid, '', None), + ('single', _('environment variable; %s') % varname, tgtid, '', None), + ] + targetnode = nodes.target('', '', ids=[tgtid]) + document.note_explicit_target(targetnode) + return [indexnode, targetnode, node], [] + + +class Target(SphinxDirective): + """ + Generic target for user-defined cross-reference types. + """ + indextemplate = '' + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + # normalize whitespace in fullname like XRefRole does + fullname = ws_re.sub(' ', self.arguments[0].strip()) + node_id = make_id(self.env, self.state.document, self.name, fullname) + node = nodes.target('', '', ids=[node_id]) + self.set_source_info(node) + self.state.document.note_explicit_target(node) + ret: list[Node] = [node] + if self.indextemplate: + indexentry = self.indextemplate % (fullname,) + indextype = 'single' + colon = indexentry.find(':') + if colon != -1: + indextype = indexentry[:colon].strip() + indexentry = indexentry[colon + 1:].strip() + inode = addnodes.index(entries=[(indextype, indexentry, node_id, '', None)]) + ret.insert(0, inode) + name = self.name + if ':' in self.name: + _, name = self.name.split(':', 1) + + std = cast(StandardDomain, self.env.get_domain('std')) + std.note_object(name, fullname, node_id, location=node) + + return ret + + +class Cmdoption(ObjectDescription[str]): + """ + Description of a command-line option (.. option). + """ + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + """Transform an option description into RST nodes.""" + count = 0 + firstname = '' + for potential_option in sig.split(', '): + potential_option = potential_option.strip() + m = option_desc_re.match(potential_option) + if not m: + logger.warning(__('Malformed option description %r, should ' + 'look like "opt", "-opt args", "--opt args", ' + '"/opt args" or "+opt args"'), potential_option, + location=signode) + continue + optname, args = m.groups() + if optname[-1] == '[' and args[-1] == ']': + # optional value surrounded by brackets (ex. foo[=bar]) + optname = optname[:-1] + args = '[' + args + + if count: + if self.env.config.option_emphasise_placeholders: + signode += addnodes.desc_sig_punctuation(',', ',') + signode += addnodes.desc_sig_space() + else: + signode += addnodes.desc_addname(', ', ', ') + signode += addnodes.desc_name(optname, optname) + if self.env.config.option_emphasise_placeholders: + add_end_bracket = False + if args: + if args[0] == '[' and args[-1] == ']': + add_end_bracket = True + signode += addnodes.desc_sig_punctuation('[', '[') + args = args[1:-1] + elif args[0] == ' ': + signode += addnodes.desc_sig_space() + args = args.strip() + elif args[0] == '=': + signode += addnodes.desc_sig_punctuation('=', '=') + args = args[1:] + for part in samp_role.parse(args): + if isinstance(part, nodes.Text): + signode += nodes.Text(part.astext()) + else: + signode += part + if add_end_bracket: + signode += addnodes.desc_sig_punctuation(']', ']') + else: + signode += addnodes.desc_addname(args, args) + if not count: + firstname = optname + signode['allnames'] = [optname] + else: + signode['allnames'].append(optname) + count += 1 + if not firstname: + raise ValueError + return firstname + + def add_target_and_index(self, firstname: str, sig: str, signode: desc_signature) -> None: + currprogram = self.env.ref_context.get('std:program') + for optname in signode.get('allnames', []): + prefixes = ['cmdoption'] + if currprogram: + prefixes.append(currprogram) + if not optname.startswith(('-', '/')): + prefixes.append('arg') + prefix = '-'.join(prefixes) + node_id = make_id(self.env, self.state.document, prefix, optname) + signode['ids'].append(node_id) + + self.state.document.note_explicit_target(signode) + + domain = self.env.domains['std'] + for optname in signode.get('allnames', []): + domain.add_program_option(currprogram, optname, + self.env.docname, signode['ids'][0]) + + # create an index entry + if currprogram: + descr = _('%s command line option') % currprogram + else: + descr = _('command line option') + for option in signode.get('allnames', []): + entry = '; '.join([descr, option]) + self.indexnode['entries'].append(('pair', entry, signode['ids'][0], '', None)) + + +class Program(SphinxDirective): + """ + Directive to name the program for which options are documented. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + program = ws_re.sub('-', self.arguments[0].strip()) + if program == 'None': + self.env.ref_context.pop('std:program', None) + else: + self.env.ref_context['std:program'] = program + return [] + + +class OptionXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, + title: str, target: str) -> tuple[str, str]: + refnode['std:program'] = env.ref_context.get('std:program') + return title, target + + +def split_term_classifiers(line: str) -> list[str | None]: + # split line into a term and classifiers. if no classifier, None is used.. + parts: list[str | None] = re.split(' +: +', line) + [None] + return parts + + +def make_glossary_term(env: BuildEnvironment, textnodes: Iterable[Node], index_key: str, + source: str, lineno: int, node_id: str | None, document: nodes.document, + ) -> nodes.term: + # get a text-only representation of the term and register it + # as a cross-reference target + term = nodes.term('', '', *textnodes) + term.source = source + term.line = lineno + termtext = term.astext() + + if node_id: + # node_id is given from outside (mainly i18n module), use it forcedly + term['ids'].append(node_id) + else: + node_id = make_id(env, document, 'term', termtext) + term['ids'].append(node_id) + document.note_explicit_target(term) + + std = cast(StandardDomain, env.get_domain('std')) + std._note_term(termtext, node_id, location=term) + + # add an index entry too + indexnode = addnodes.index() + indexnode['entries'] = [('single', termtext, node_id, 'main', index_key)] + indexnode.source, indexnode.line = term.source, term.line + term.append(indexnode) + + return term + + +class Glossary(SphinxDirective): + """ + Directive to create a glossary with cross-reference targets for :term: + roles. + """ + + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec: OptionSpec = { + 'sorted': directives.flag, + } + + def run(self) -> list[Node]: + node = addnodes.glossary() + node.document = self.state.document + node['sorted'] = ('sorted' in self.options) + + # This directive implements a custom format of the reST definition list + # that allows multiple lines of terms before the definition. This is + # easy to parse since we know that the contents of the glossary *must + # be* a definition list. + + # first, collect single entries + entries: list[tuple[list[tuple[str, str, int]], StringList]] = [] + in_definition = True + in_comment = False + was_empty = True + messages: list[Node] = [] + for line, (source, lineno) in zip(self.content, self.content.items): + # empty line -> add to last definition + if not line: + if in_definition and entries: + entries[-1][1].append('', source, lineno) + was_empty = True + continue + # unindented line -> a term + if line and not line[0].isspace(): + # enable comments + if line.startswith('.. '): + in_comment = True + continue + in_comment = False + + # first term of definition + if in_definition: + if not was_empty: + messages.append(self.state.reporter.warning( + _('glossary term must be preceded by empty line'), + source=source, line=lineno)) + entries.append(([(line, source, lineno)], StringList())) + in_definition = False + # second term and following + else: + if was_empty: + messages.append(self.state.reporter.warning( + _('glossary terms must not be separated by empty lines'), + source=source, line=lineno)) + if entries: + entries[-1][0].append((line, source, lineno)) + else: + messages.append(self.state.reporter.warning( + _('glossary seems to be misformatted, check indentation'), + source=source, line=lineno)) + elif in_comment: + pass + else: + if not in_definition: + # first line of definition, determines indentation + in_definition = True + indent_len = len(line) - len(line.lstrip()) + if entries: + entries[-1][1].append(line[indent_len:], source, lineno) + else: + messages.append(self.state.reporter.warning( + _('glossary seems to be misformatted, check indentation'), + source=source, line=lineno)) + was_empty = False + + # now, parse all the entries into a big definition list + items: list[nodes.definition_list_item] = [] + for terms, definition in entries: + termnodes: list[Node] = [] + system_messages: list[Node] = [] + for line, source, lineno in terms: + parts = split_term_classifiers(line) + # parse the term with inline markup + # classifiers (parts[1:]) will not be shown on doctree + textnodes, sysmsg = self.state.inline_text(parts[0], # type: ignore[arg-type] + lineno) + + # use first classifier as a index key + term = make_glossary_term(self.env, textnodes, + parts[1], source, lineno, # type: ignore[arg-type] + node_id=None, document=self.state.document) + term.rawsource = line + system_messages.extend(sysmsg) + termnodes.append(term) + + termnodes.extend(system_messages) + + defnode = nodes.definition() + if definition: + self.state.nested_parse(definition, definition.items[0][1], + defnode) + termnodes.append(defnode) + items.append(nodes.definition_list_item('', *termnodes)) + + dlist = nodes.definition_list('', *items) + dlist['classes'].append('glossary') + node += dlist + return messages + [node] + + +def token_xrefs(text: str, productionGroup: str = '') -> list[Node]: + if len(productionGroup) != 0: + productionGroup += ':' + retnodes: list[Node] = [] + pos = 0 + for m in token_re.finditer(text): + if m.start() > pos: + txt = text[pos:m.start()] + retnodes.append(nodes.Text(txt)) + token = m.group(1) + if ':' in token: + if token[0] == '~': + _, title = token.split(':') + target = token[1:] + elif token[0] == ':': + title = token[1:] + target = title + else: + title = token + target = token + else: + title = token + target = productionGroup + token + refnode = pending_xref(title, reftype='token', refdomain='std', + reftarget=target) + refnode += nodes.literal(token, title, classes=['xref']) + retnodes.append(refnode) + pos = m.end() + if pos < len(text): + retnodes.append(nodes.Text(text[pos:])) + return retnodes + + +class ProductionList(SphinxDirective): + """ + Directive to list grammar productions. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec: OptionSpec = {} + + def run(self) -> list[Node]: + domain = cast(StandardDomain, self.env.get_domain('std')) + node: Element = addnodes.productionlist() + self.set_source_info(node) + # The backslash handling is from ObjectDescription.get_signatures + nl_escape_re = re.compile(r'\\\n') + lines = nl_escape_re.sub('', self.arguments[0]).split('\n') + + productionGroup = "" + first_rule_seen = False + for rule in lines: + if not first_rule_seen and ':' not in rule: + productionGroup = rule.strip() + continue + first_rule_seen = True + try: + name, tokens = rule.split(':', 1) + except ValueError: + break + subnode = addnodes.production(rule) + name = name.strip() + subnode['tokenname'] = name + if subnode['tokenname']: + prefix = 'grammar-token-%s' % productionGroup + node_id = make_id(self.env, self.state.document, prefix, name) + subnode['ids'].append(node_id) + self.state.document.note_implicit_target(subnode, subnode) + + if len(productionGroup) != 0: + objName = f"{productionGroup}:{name}" + else: + objName = name + domain.note_object('token', objName, node_id, location=node) + subnode.extend(token_xrefs(tokens, productionGroup)) + node.append(subnode) + return [node] + + +class TokenXRefRole(XRefRole): + def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, + title: str, target: str) -> tuple[str, str]: + target = target.lstrip('~') # a title-specific thing + if not self.has_explicit_title and title[0] == '~': + if ':' in title: + _, title = title.split(':') + else: + title = title[1:] + return title, target + + +class StandardDomain(Domain): + """ + Domain for all objects that don't fit into another domain or are added + via the application interface. + """ + + name = 'std' + label = 'Default' + + object_types: dict[str, ObjType] = { + 'term': ObjType(_('glossary term'), 'term', searchprio=-1), + 'token': ObjType(_('grammar token'), 'token', searchprio=-1), + 'label': ObjType(_('reference label'), 'ref', 'keyword', + searchprio=-1), + 'envvar': ObjType(_('environment variable'), 'envvar'), + 'cmdoption': ObjType(_('program option'), 'option'), + 'doc': ObjType(_('document'), 'doc', searchprio=-1), + } + + directives: dict[str, type[Directive]] = { + 'program': Program, + 'cmdoption': Cmdoption, # old name for backwards compatibility + 'option': Cmdoption, + 'envvar': EnvVar, + 'glossary': Glossary, + 'productionlist': ProductionList, + } + roles: dict[str, RoleFunction | XRefRole] = { + 'option': OptionXRefRole(warn_dangling=True), + 'envvar': EnvVarXRefRole(), + # links to tokens in grammar productions + 'token': TokenXRefRole(), + # links to terms in glossary + 'term': XRefRole(innernodeclass=nodes.inline, + warn_dangling=True), + # links to headings or arbitrary labels + 'ref': XRefRole(lowercase=True, innernodeclass=nodes.inline, + warn_dangling=True), + # links to labels of numbered figures, tables and code-blocks + 'numref': XRefRole(lowercase=True, + warn_dangling=True), + # links to labels, without a different title + 'keyword': XRefRole(warn_dangling=True), + # links to documents + 'doc': XRefRole(warn_dangling=True, innernodeclass=nodes.inline), + } + + initial_data: Final = { # type: ignore[misc] + 'progoptions': {}, # (program, name) -> docname, labelid + 'objects': {}, # (type, name) -> docname, labelid + 'labels': { # labelname -> docname, labelid, sectionname + 'genindex': ('genindex', '', _('Index')), + 'modindex': ('py-modindex', '', _('Module Index')), + 'search': ('search', '', _('Search Page')), + }, + 'anonlabels': { # labelname -> docname, labelid + 'genindex': ('genindex', ''), + 'modindex': ('py-modindex', ''), + 'search': ('search', ''), + }, + } + + _virtual_doc_names: dict[str, tuple[str, str]] = { # labelname -> docname, sectionname + 'genindex': ('genindex', _('Index')), + 'modindex': ('py-modindex', _('Module Index')), + 'search': ('search', _('Search Page')), + } + + dangling_warnings = { + 'term': 'term not in glossary: %(target)r', + 'numref': 'undefined label: %(target)r', + 'keyword': 'unknown keyword: %(target)r', + 'doc': 'unknown document: %(target)r', + 'option': 'unknown option: %(target)r', + } + + # node_class -> (figtype, title_getter) + enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = { + nodes.figure: ('figure', None), + nodes.table: ('table', None), + nodes.container: ('code-block', None), + } + + def __init__(self, env: BuildEnvironment) -> None: + super().__init__(env) + + # set up enumerable nodes + self.enumerable_nodes = copy(self.enumerable_nodes) # create a copy for this instance + for node, settings in env.app.registry.enumerable_nodes.items(): + self.enumerable_nodes[node] = settings + + def note_hyperlink_target(self, name: str, docname: str, node_id: str, + title: str = '') -> None: + """Add a hyperlink target for cross reference. + + .. warning:: + + This is only for internal use. Please don't use this from your extension. + ``document.note_explicit_target()`` or ``note_implicit_target()`` are recommended to + add a hyperlink target to the document. + + This only adds a hyperlink target to the StandardDomain. And this does not add a + node_id to node. Therefore, it is very fragile to calling this without + understanding hyperlink target framework in both docutils and Sphinx. + + .. versionadded:: 3.0 + """ + if name in self.anonlabels and self.anonlabels[name] != (docname, node_id): + logger.warning(__('duplicate label %s, other instance in %s'), + name, self.env.doc2path(self.anonlabels[name][0])) + + self.anonlabels[name] = (docname, node_id) + if title: + self.labels[name] = (docname, node_id, title) + + @property + def objects(self) -> dict[tuple[str, str], tuple[str, str]]: + return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid + + def note_object(self, objtype: str, name: str, labelid: str, location: Any = None, + ) -> None: + """Note a generic object for cross reference. + + .. versionadded:: 3.0 + """ + if (objtype, name) in self.objects: + docname = self.objects[objtype, name][0] + logger.warning(__('duplicate %s description of %s, other instance in %s'), + objtype, name, docname, location=location) + self.objects[objtype, name] = (self.env.docname, labelid) + + @property + def _terms(self) -> dict[str, tuple[str, str]]: + """.. note:: Will be removed soon. internal use only.""" + return self.data.setdefault('terms', {}) # (name) -> docname, labelid + + def _note_term(self, term: str, labelid: str, location: Any = None) -> None: + """Note a term for cross reference. + + .. note:: Will be removed soon. internal use only. + """ + self.note_object('term', term, labelid, location) + + self._terms[term.lower()] = (self.env.docname, labelid) + + @property + def progoptions(self) -> dict[tuple[str | None, str], tuple[str, str]]: + return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid + + @property + def labels(self) -> dict[str, tuple[str, str, str]]: + return self.data.setdefault('labels', {}) # labelname -> docname, labelid, sectionname + + @property + def anonlabels(self) -> dict[str, tuple[str, str]]: + return self.data.setdefault('anonlabels', {}) # labelname -> docname, labelid + + def clear_doc(self, docname: str) -> None: + key: Any = None + for key, (fn, _l) in list(self.progoptions.items()): + if fn == docname: + del self.progoptions[key] + for key, (fn, _l) in list(self.objects.items()): + if fn == docname: + del self.objects[key] + for key, (fn, _l) in list(self._terms.items()): + if fn == docname: + del self._terms[key] + for key, (fn, _l, _l) in list(self.labels.items()): + if fn == docname: + del self.labels[key] + for key, (fn, _l) in list(self.anonlabels.items()): + if fn == docname: + del self.anonlabels[key] + + def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None: + # XXX duplicates? + for key, data in otherdata['progoptions'].items(): + if data[0] in docnames: + self.progoptions[key] = data + for key, data in otherdata['objects'].items(): + if data[0] in docnames: + self.objects[key] = data + for key, data in otherdata['terms'].items(): + if data[0] in docnames: + self._terms[key] = data + for key, data in otherdata['labels'].items(): + if data[0] in docnames: + self.labels[key] = data + for key, data in otherdata['anonlabels'].items(): + if data[0] in docnames: + self.anonlabels[key] = data + + def process_doc( + self, env: BuildEnvironment, docname: str, document: nodes.document, + ) -> None: + for name, explicit in document.nametypes.items(): + if not explicit: + continue + labelid = document.nameids[name] + if labelid is None: + continue + node = document.ids[labelid] + if isinstance(node, nodes.target) and 'refid' in node: + # indirect hyperlink targets + node = document.ids.get(node['refid']) # type: ignore[assignment] + labelid = node['names'][0] + if (node.tagname == 'footnote' or + 'refuri' in node or + node.tagname.startswith('desc_')): + # ignore footnote labels, labels automatically generated from a + # link and object descriptions + continue + if name in self.labels: + logger.warning(__('duplicate label %s, other instance in %s'), + name, env.doc2path(self.labels[name][0]), + location=node) + self.anonlabels[name] = docname, labelid + if node.tagname == 'section': + title = cast(nodes.title, node[0]) + sectname = clean_astext(title) + elif node.tagname == 'rubric': + sectname = clean_astext(node) + elif self.is_enumerable_node(node): + sectname = self.get_numfig_title(node) or '' + if not sectname: + continue + else: + if (isinstance(node, (nodes.definition_list, + nodes.field_list)) and + node.children): + node = cast(nodes.Element, node.children[0]) + if isinstance(node, (nodes.field, nodes.definition_list_item)): + node = cast(nodes.Element, node.children[0]) + if isinstance(node, (nodes.term, nodes.field_name)): + sectname = clean_astext(node) + else: + toctree = next(node.findall(addnodes.toctree), None) + if toctree and toctree.get('caption'): + sectname = toctree['caption'] + else: + # anonymous-only labels + continue + self.labels[name] = docname, labelid, sectname + + def add_program_option(self, program: str | None, name: str, + docname: str, labelid: str) -> None: + # prefer first command option entry + if (program, name) not in self.progoptions: + self.progoptions[program, name] = (docname, labelid) + + def build_reference_node(self, fromdocname: str, builder: Builder, docname: str, + labelid: str, sectname: str, rolename: str, **options: Any, + ) -> Element: + nodeclass = options.pop('nodeclass', nodes.reference) + newnode = nodeclass('', '', internal=True, **options) + innernode = nodes.inline(sectname, sectname) + if innernode.get('classes') is not None: + innernode['classes'].append('std') + innernode['classes'].append('std-' + rolename) + if docname == fromdocname: + newnode['refid'] = labelid + else: + # set more info in contnode; in case the + # get_relative_uri call raises NoUri, + # the builder will then have to resolve these + contnode = pending_xref('') + contnode['refdocname'] = docname + contnode['refsectname'] = sectname + newnode['refuri'] = builder.get_relative_uri( + fromdocname, docname) + if labelid: + newnode['refuri'] += '#' + labelid + newnode.append(innernode) + return newnode + + def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element, + ) -> Element | None: + if typ == 'ref': + resolver = self._resolve_ref_xref + elif typ == 'numref': + resolver = self._resolve_numref_xref + elif typ == 'keyword': + resolver = self._resolve_keyword_xref + elif typ == 'doc': + resolver = self._resolve_doc_xref + elif typ == 'option': + resolver = self._resolve_option_xref + elif typ == 'term': + resolver = self._resolve_term_xref + else: + resolver = self._resolve_obj_xref + + return resolver(env, fromdocname, builder, typ, target, node, contnode) + + def _resolve_ref_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, node: pending_xref, + contnode: Element) -> Element | None: + if node['refexplicit']: + # reference to anonymous label; the reference uses + # the supplied link caption + docname, labelid = self.anonlabels.get(target, ('', '')) + sectname = node.astext() + else: + # reference to named label; the final node will + # contain the section name after the label + docname, labelid, sectname = self.labels.get(target, ('', '', '')) + if not docname: + return None + + return self.build_reference_node(fromdocname, builder, + docname, labelid, sectname, 'ref') + + def _resolve_numref_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + if target in self.labels: + docname, labelid, figname = self.labels.get(target, ('', '', '')) + else: + docname, labelid = self.anonlabels.get(target, ('', '')) + figname = None + + if not docname: + return None + + target_node = env.get_doctree(docname).ids.get(labelid) + assert target_node is not None + figtype = self.get_enumerable_node_type(target_node) + if figtype is None: + return None + + if figtype != 'section' and env.config.numfig is False: + logger.warning(__('numfig is disabled. :numref: is ignored.'), location=node) + return contnode + + try: + fignumber = self.get_fignumber(env, builder, figtype, docname, target_node) + if fignumber is None: + return contnode + except ValueError: + logger.warning(__("Failed to create a cross reference. Any number is not " + "assigned: %s"), + labelid, location=node) + return contnode + + try: + if node['refexplicit']: + title = contnode.astext() + else: + title = env.config.numfig_format.get(figtype, '') + + if figname is None and '{name}' in title: + logger.warning(__('the link has no caption: %s'), title, location=node) + return contnode + else: + fignum = '.'.join(map(str, fignumber)) + if '{name}' in title or 'number' in title: + # new style format (cf. "Fig.{number}") + if figname: + newtitle = title.format(name=figname, number=fignum) + else: + newtitle = title.format(number=fignum) + else: + # old style format (cf. "Fig.%s") + newtitle = title % fignum + except KeyError as exc: + logger.warning(__('invalid numfig_format: %s (%r)'), title, exc, location=node) + return contnode + except TypeError: + logger.warning(__('invalid numfig_format: %s'), title, location=node) + return contnode + + return self.build_reference_node(fromdocname, builder, + docname, labelid, newtitle, 'numref', + nodeclass=addnodes.number_reference, + title=title) + + def _resolve_keyword_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + # keywords are oddballs: they are referenced by named labels + docname, labelid, _ = self.labels.get(target, ('', '', '')) + if not docname: + return None + return make_refnode(builder, fromdocname, docname, + labelid, contnode) + + def _resolve_doc_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + # directly reference to document by source name; can be absolute or relative + refdoc = node.get('refdoc', fromdocname) + docname = docname_join(refdoc, node['reftarget']) + if docname not in env.all_docs: + return None + else: + if node['refexplicit']: + # reference with explicit title + caption = node.astext() + else: + caption = clean_astext(env.titles[docname]) + innernode = nodes.inline(caption, caption, classes=['doc']) + return make_refnode(builder, fromdocname, docname, None, innernode) + + def _resolve_option_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + progname = node.get('std:program') + target = target.strip() + docname, labelid = self.progoptions.get((progname, target), ('', '')) + if not docname: + # Support also reference that contain an option value: + # * :option:`-foo=bar` + # * :option:`-foo[=bar]` + # * :option:`-foo bar` + for needle in {'=', '[=', ' '}: + if needle in target: + stem, _, _ = target.partition(needle) + docname, labelid = self.progoptions.get((progname, stem), ('', '')) + if docname: + break + if not docname: + commands = [] + while ws_re.search(target): + subcommand, target = ws_re.split(target, 1) + commands.append(subcommand) + progname = "-".join(commands) + + docname, labelid = self.progoptions.get((progname, target), ('', '')) + if docname: + break + else: + return None + + return make_refnode(builder, fromdocname, docname, + labelid, contnode) + + def _resolve_term_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + result = self._resolve_obj_xref(env, fromdocname, builder, typ, + target, node, contnode) + if result: + return result + else: + # fallback to case insensitive match + if target.lower() in self._terms: + docname, labelid = self._terms[target.lower()] + return make_refnode(builder, fromdocname, docname, labelid, contnode) + else: + return None + + def _resolve_obj_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, typ: str, target: str, + node: pending_xref, contnode: Element) -> Element | None: + objtypes = self.objtypes_for_role(typ) or [] + for objtype in objtypes: + if (objtype, target) in self.objects: + docname, labelid = self.objects[objtype, target] + break + else: + docname, labelid = '', '' + if not docname: + return None + return make_refnode(builder, fromdocname, docname, + labelid, contnode) + + def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, + builder: Builder, target: str, node: pending_xref, + contnode: Element) -> list[tuple[str, Element]]: + results: list[tuple[str, Element]] = [] + ltarget = target.lower() # :ref: lowercases its target automatically + for role in ('ref', 'option'): # do not try "keyword" + res = self.resolve_xref(env, fromdocname, builder, role, + ltarget if role == 'ref' else target, + node, contnode) + if res: + results.append(('std:' + role, res)) + # all others + for objtype in self.object_types: + key = (objtype, target) + if objtype == 'term': + key = (objtype, ltarget) + if key in self.objects: + docname, labelid = self.objects[key] + role = 'std:' + self.role_for_objtype(objtype) # type: ignore[operator] + results.append((role, make_refnode(builder, fromdocname, docname, + labelid, contnode))) + return results + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + # handle the special 'doc' reference here + for doc in self.env.all_docs: + yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1) + for (prog, option), info in self.progoptions.items(): + if prog: + fullname = ".".join([prog, option]) + yield (fullname, fullname, 'cmdoption', info[0], info[1], 1) + else: + yield (option, option, 'cmdoption', info[0], info[1], 1) + for (type, name), info in self.objects.items(): + yield (name, name, type, info[0], info[1], + self.object_types[type].attrs['searchprio']) + for name, (docname, labelid, sectionname) in self.labels.items(): + yield (name, sectionname, 'label', docname, labelid, -1) + # add anonymous-only labels as well + non_anon_labels = set(self.labels) + for name, (docname, labelid) in self.anonlabels.items(): + if name not in non_anon_labels: + yield (name, name, 'label', docname, labelid, -1) + + def get_type_name(self, type: ObjType, primary: bool = False) -> str: + # never prepend "Default" + return type.lname + + def is_enumerable_node(self, node: Node) -> bool: + return node.__class__ in self.enumerable_nodes + + def get_numfig_title(self, node: Node) -> str | None: + """Get the title of enumerable nodes to refer them using its title""" + if self.is_enumerable_node(node): + elem = cast(Element, node) + _, title_getter = self.enumerable_nodes.get(elem.__class__, (None, None)) + if title_getter: + return title_getter(elem) + else: + for subnode in elem: + if isinstance(subnode, (nodes.caption, nodes.title)): + return clean_astext(subnode) + + return None + + def get_enumerable_node_type(self, node: Node) -> str | None: + """Get type of enumerable nodes.""" + def has_child(node: Element, cls: type) -> bool: + return any(isinstance(child, cls) for child in node) + + if isinstance(node, nodes.section): + return 'section' + elif (isinstance(node, nodes.container) and + 'literal_block' in node and + has_child(node, nodes.literal_block)): + # given node is a code-block having caption + return 'code-block' + else: + figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) + return figtype + + def get_fignumber( + self, + env: BuildEnvironment, + builder: Builder, + figtype: str, + docname: str, + target_node: Element, + ) -> tuple[int, ...] | None: + if figtype == 'section': + if builder.name == 'latex': + return () + elif docname not in env.toc_secnumbers: + raise ValueError # no number assigned + else: + anchorname = '#' + target_node['ids'][0] + if anchorname not in env.toc_secnumbers[docname]: + # try first heading which has no anchor + return env.toc_secnumbers[docname].get('') + else: + return env.toc_secnumbers[docname].get(anchorname) + else: + try: + figure_id = target_node['ids'][0] + return env.toc_fignumbers[docname][figtype][figure_id] + except (KeyError, IndexError) as exc: + # target_node is found, but fignumber is not assigned. + # Maybe it is defined in orphaned document. + raise ValueError from exc + + def get_full_qualified_name(self, node: Element) -> str | None: + if node.get('reftype') == 'option': + progname = node.get('std:program') + command = ws_re.split(node.get('reftarget')) + if progname: + command.insert(0, progname) + option = command.pop() + if command: + return '.'.join(['-'.join(command), option]) + else: + return None + else: + return None + + +def warn_missing_reference(app: Sphinx, domain: Domain, node: pending_xref, + ) -> bool | None: + if (domain and domain.name != 'std') or node['reftype'] != 'ref': + return None + else: + target = node['reftarget'] + if target not in domain.anonlabels: # type: ignore[attr-defined] + msg = __('undefined label: %r') + else: + msg = __('Failed to create a cross reference. A title or caption not found: %r') + + logger.warning(msg % target, location=node, type='ref', subtype=node['reftype']) + return True + + +def setup(app: Sphinx) -> dict[str, Any]: + app.add_domain(StandardDomain) + app.connect('warn-missing-reference', warn_missing_reference) + + return { + 'version': 'builtin', + 'env_version': 2, + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } |