summaryrefslogtreecommitdiffstats
path: root/sphinx/domains
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/domains')
-rw-r--r--sphinx/domains/__init__.py406
-rw-r--r--sphinx/domains/c.py3906
-rw-r--r--sphinx/domains/changeset.py161
-rw-r--r--sphinx/domains/citation.py154
-rw-r--r--sphinx/domains/cpp.py8233
-rw-r--r--sphinx/domains/index.py126
-rw-r--r--sphinx/domains/javascript.py508
-rw-r--r--sphinx/domains/math.py152
-rw-r--r--sphinx/domains/python.py1769
-rw-r--r--sphinx/domains/rst.py299
-rw-r--r--sphinx/domains/std.py1123
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,
+ }