diff options
Diffstat (limited to 'sphinx/domains/c.py')
-rw-r--r-- | sphinx/domains/c.py | 3906 |
1 files changed, 3906 insertions, 0 deletions
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, + } |