1"""The code a marker is attached to: what counts as an entity, and how one is named.
   2
   3A marker is attached to a definition node of the syntax tree (a function, a class,
   4a namespace, ...): its *entity*. This module holds the per-language tables that say
   5which node kinds are entities, and the one naming function, :func:`describe_entity`,
   6that turns such a node into its kind, its qualified name *within its file* (no
   7module or package prefix: that is derived from the file's path, which this module
   8never reads -- every function here is pure) and that name's components, its
   9``semantic``. :func:`render_semantic` joins the components back into the name by the
  10rule of the file's language (:func:`scope_language`), and :func:`name_chain` is what
  11identifies an entity among them: names, never kinds.
  12
  13The traversal that picks the node for a comment is ``find_associated_scope`` /
  14``associate`` in :mod:`sphinx_codelinks.analyse.utils`; this module only answers
  15questions about a node.
  16"""
  17
  18from __future__ import annotations
  19
  20import re
  21from collections.abc import Callable, Iterable, Sequence
  22from dataclasses import dataclass, field, replace
  23from pathlib import PurePath
  24from typing import Literal, NotRequired, TypedDict
  25
  26from tree_sitter import Node as TreeSitterNode
  27
  28from sphinx_codelinks.analyse.cpp_macros import MacroScopes
  29from sphinx_codelinks.source_discover.config import CommentType
  30
  31EntityKind = Literal[
  32    "function",
  33    "method",
  34    "constructor",
  35    "destructor",
  36    "operator",
  37    "class",
  38    "struct",
  39    "union",
  40    "enum",
  41    "interface",
  42    "trait",
  43    "impl",
  44    "record",
  45    "namespace",
  46    "module",
  47    "type",
  48    "macro",
  49    "property",
  50    "event",
  51    "indexer",
  52    "delegate",
  53    "test",
  54    "key",
  55    "item",
  56]
  57"""The kind of an entity, as the analysis output spells it."""
  58
  59Mechanism = Literal["doc_comment", "leading", "trailing", "enclosing"]
  60"""How a marker came to be attached to its entity.
  61
  62``doc_comment`` is *declared*: a documentation comment (``///``, ``//!``, ``/**``,
  63``/*!``, a C# ``///``, a JSDoc ``/**``, a Python docstring, PowerShell comment-based
  64help first in a function's body, a VHDL ``--!``) attached by its own rule.
  65The other three are *inferred* from where a plain comment sits: on the lines before the
  66entity (``leading``), after code on the entity's own line (``trailing``), or inside its
  67body (``enclosing``).
  68"""
  69
  70
  71class SemanticElement(TypedDict):
  72    """One component of an entity's qualified name: a container, or the entity."""
  73
  74    kind: str
  75    """What it is: an :data:`EntityKind`, or ``scope`` for a C++ qualifier the syntax
  76    cannot classify (the ``ns`` of ``void ns::W::f() {}``) and for a VHDL design
  77    element no kind fits (a process, a signal). A Rust impl is an ``impl``, a Go
  78    receiver's type a ``type``."""
  79    name: str
  80    """Its name, as the qualified name spells it: ``Box<bool>`` keeps the arguments of
  81    an explicit specialisation, an expression name (``module.exports.f``) is whole,
  82    and a Rust impl is its self type (``Widget``, ``dyn Shape``)."""
  83    trait: NotRequired[str]
  84    """The trait of a Rust trait impl, on its ``impl`` component."""
  85
  86
  87@dataclass(frozen=True)
  88class EntityName:
  89    """A definition node, named."""
  90
  91    kind: EntityKind | None
  92    """What the node defines; ``None`` for a VHDL design element no kind fits (a
  93    process, a signal, a port), which is named all the same."""
  94    qualified_name: str | None
  95    """The name within its file, qualified by the containers the file shows
  96    (``ns::Widget::draw``, ``Shape.area``, ``<Widget as Shape>::area``); ``None`` when
  97    the entity has no name (an anonymous class, a data-file entry)."""
  98    semantic: tuple[SemanticElement, ...] | None
  99    """``qualified_name`` as its components, outermost first and the entity last
 100    (``namespace ns``, ``class Widget``, ``method draw``); ``None`` exactly when
 101    ``qualified_name`` is. :func:`render_semantic` spells them back."""
 102    name_end: int | None = field(default=None, compare=False, repr=False)
 103    """The byte offset where the node's own name ends in the file (C and C++ only),
 104    ``None`` when it has no name node: what the names a whole C++ file decides are
 105    keyed by (:class:`CppNames`). Not part of the analysis output."""
 106
 107
 108@dataclass(frozen=True)
 109class Association:
 110    """The node a comment is attached to, and how."""
 111
 112    node: TreeSitterNode
 113    mechanism: Mechanism
 114
 115
 116class CodeScope(TypedDict):
 117    """The ``scope`` object of an analysis record: the entity a marker is attached to."""
 118
 119    kind: str
 120    """The tree-sitter node kind of the entity (``function_definition``)."""
 121    start: int
 122    """The node's first line (1-based)."""
 123    end: int
 124    """The node's last line (1-based)."""
 125    language: str
 126    """The file's language (:func:`scope_language`): the rule ``semantic`` renders by."""
 127    entity_kind: str | None
 128    """The entity's kind (:data:`EntityKind`), ``None`` if it could not be named, or,
 129    for a VHDL design element no kind fits, if it has none."""
 130    qualified_name: str | None
 131    """The entity's in-file qualified name, ``None`` if it has none."""
 132    semantic: list[SemanticElement] | None
 133    """The qualified name's components (:class:`SemanticElement`), outermost first and
 134    the entity last; ``None`` exactly when ``qualified_name`` is."""
 135    mechanism: Mechanism
 136    """How the marker was attached (:data:`Mechanism`)."""
 137
 138
 139# ---------------------------------------------------------------------------
 140# What is an entity: the node kinds a marker can be attached to
 141# ---------------------------------------------------------------------------
 142#
 143# YAML and JSONC are intentionally absent. They are data formats, not code, so a
 144# comment associates with the surrounding data structure (key/value pair, list
 145# item, or scalar) rather than with an enclosing or following declaration. That
 146# needs a different algorithm (inline same-row association first, scalar targets,
 147# grammar-specific traversal), implemented in find_yaml_associated_structure and
 148# find_jsonc_associated_structure and dispatched from find_associated_scope.
 149# Those bespoke finders never read this table, so an entry here would be dead.
 150#
 151# Deliberately no entities yet: data members and variables, Python assignments
 152# (a lambda bound to a name included), Rust ``const``/``static``, Go ``var``/``const``,
 153# TypeScript plain consts, and macro invocations. A marker above one of them is
 154# attached to the enclosing entity, or to the file.
 155#
 156# VHDL is the deliberate exception: its signals, constants, variables, files and
 157# aliases, and the generics, ports and parameters of its interface lists, ARE entities,
 158# because a hardware design traces its ports and signals. They have no entity kind
 159# (see _VHDL_KINDS), and are named where they declare one name.
 160ENTITY_NODE_TYPES: dict[CommentType, set[str]] = {
[docs] 161    # @Python Scope Node Types, IMPL_PY_2, impl, [FE_PY]
 162    CommentType.python: {"function_definition", "class_definition"},
[docs] 163    # @C and C++ Scope Node Types, IMPL_C_2, impl, [FE_C_SUPPORT, FE_CPP]
 164    # A declaration, a field declaration, a class, struct, union or enum is an entity
 165    # only when its predicate holds: see ENTITY_PREDICATES.
 166    CommentType.cpp: {
 167        "function_definition",
 168        "declaration",
 169        "field_declaration",
 170        "class_specifier",
 171        "struct_specifier",
 172        "union_specifier",
 173        "enum_specifier",
 174        "namespace_definition",
 175    },
 176    CommentType.cs: {
 177        "method_declaration",
 178        "class_declaration",
 179        "property_declaration",
 180        "constructor_declaration",
 181        "destructor_declaration",
 182        "struct_declaration",
 183        "interface_declaration",
 184        "enum_declaration",
 185        "record_declaration",
 186        "operator_declaration",
 187        "conversion_operator_declaration",
 188        "indexer_declaration",
 189        "delegate_declaration",
 190        "event_declaration",
 191        "local_function_statement",
 192        "namespace_declaration",
 193        "file_scoped_namespace_declaration",
 194    },
[docs] 195    # @TypeScript Scope Node Types, IMPL_TS_2, impl, [FE_TS]
 196    # The last five are scopes only when they hold a function or a class: see
 197    # ENTITY_PREDICATES.
 198    CommentType.ts: {
 199        "function_declaration",
 200        "generator_function_declaration",
 201        "function_signature",
 202        "class_declaration",
 203        "abstract_class_declaration",
 204        "interface_declaration",
 205        "type_alias_declaration",
 206        "enum_declaration",
 207        "module",
 208        "internal_module",
 209        "method_definition",
 210        "method_signature",
 211        "abstract_method_signature",
 212        "lexical_declaration",
 213        "variable_declaration",
 214        "public_field_definition",
 215        "assignment_expression",
 216        "export_statement",
 217    },
[docs] 218    # @Rust Scope Node Types, IMPL_RUST_2, impl, [FE_RUST]
 219    CommentType.rust: {
 220        "function_item",
 221        "function_signature_item",
 222        "struct_item",
 223        "enum_item",
 224        "union_item",
 225        "impl_item",
 226        "trait_item",
 227        "mod_item",
 228        "type_item",
 229        "macro_definition",
 230    },
[docs] 231    # @Go Scope Node Types, IMPL_GO_4, impl, [FE_GO]
 232    CommentType.go: {
 233        "function_declaration",
 234        "method_declaration",
 235        "type_declaration",
 236        "type_spec",
 237        "method_elem",
 238    },
[docs] 239    # @Bash Scope Node Types, IMPL_BASH_2, impl, [FE_BASH]
 240    CommentType.bash: {"function_definition"},
[docs] 241    # @PowerShell Scope Node Types, IMPL_PS_2, impl, [FE_POWERSHELL]
 242    # A command is an entity only when it is a Pester block: see ENTITY_PREDICATES.
 243    CommentType.powershell: {
 244        "function_statement",
 245        "class_statement",
 246        "class_method_definition",
 247        "class_property_definition",
 248        "enum_statement",
 249        "command",
 250    },
[docs] 251    # @VHDL Scope Node Types, IMPL_VHDL_2, impl, [FE_VHDL]
 252    # VHDL's design elements, data elements included (see above). Sequential
 253    # statements are not among them: a marker among those documents its process or
 254    # subprogram.
 255    CommentType.vhdl: {
 256        # design units
 257        "entity_declaration",
 258        "architecture_definition",
 259        "package_declaration",
 260        "package_definition",
 261        "package_instantiation_declaration",
 262        "configuration_declaration",
 263        "context_declaration",
 264        # subprograms
 265        "subprogram_declaration",
 266        "subprogram_definition",
 267        "subprogram_instantiation_declaration",
 268        # declarations
 269        "type_declaration",
 270        "subtype_declaration",
 271        "component_declaration",
 272        "constant_declaration",
 273        "signal_declaration",
 274        "variable_declaration",
 275        "file_declaration",
 276        "alias_declaration",
 277        # generics, ports and parameters
 278        "interface_declaration",
 279        "interface_constant_declaration",
 280        "interface_signal_declaration",
 281        "interface_variable_declaration",
 282        "interface_file_declaration",
 283        "interface_type_declaration",
 284        "interface_subprogram_declaration",
 285        "interface_package_declaration",
 286        # concurrent statements
 287        "process_statement",
 288        "block_statement",
 289        "for_generate_statement",
 290        "if_generate_statement",
 291        "case_generate_statement",
 292        "component_instantiation_statement",
 293        "concurrent_simple_signal_assignment",
 294        "concurrent_conditional_signal_assignment",
 295        "concurrent_selected_signal_assignment",
 296        "concurrent_assertion_statement",
 297        "concurrent_procedure_call_statement",
 298    },
 299}
 300SCOPE_NODE_TYPES = ENTITY_NODE_TYPES
 301"""The name this table had before entities were named; kept for importers."""
 302
 303# What a TypeScript declaration's value can be wrapped in without changing what it
 304# is: ``(() => {}) as Handler``, ``<Handler>(() => {})``, ``f!``, ``x satisfies T``.
 305TS_VALUE_WRAPPER_TYPES = {
 306    "parenthesized_expression",
 307    "as_expression",
 308    "satisfies_expression",
 309    "non_null_expression",
 310    "type_assertion",
 311}
 312# The values that make a declaration a scope.
 313TS_FUNCTION_VALUE_TYPES = {
 314    "arrow_function",
 315    "function_expression",
 316    "generator_function",
 317    "class",
 318}
 319
 320
 321def _unwrap_ts_value(value: TreeSitterNode | None) -> TreeSitterNode | None:
 322    """``value`` seen through the wrappers of :data:`TS_VALUE_WRAPPER_TYPES`."""
 323    while value is not None and value.type in TS_VALUE_WRAPPER_TYPES:
 324        children = value.named_children
 325        if not children:
 326            return None
 327        # ``<T>x`` names its type first, every other wrapper its expression
 328        value = children[-1] if value.type == "type_assertion" else children[0]
 329    return value
 330
 331
 332def _is_function_like(value: TreeSitterNode | None) -> bool:
 333    """True if ``value``, seen through its wrappers, is a function or a class."""
 334    value = _unwrap_ts_value(value)
 335    return value is not None and value.type in TS_FUNCTION_VALUE_TYPES
 336
 337
 338def _declares_function(node: TreeSitterNode) -> bool:
 339    """A ``const``/``let``/``var`` declaration one of whose declarators is a function,
 340    or one declared with ``declare`` (``declare const x: T``), which holds no value and
 341    is the declared thing itself. Only the ``declare``-prefixed form: a variable inside a
 342    ``declare namespace``, ``module`` or ``global`` body, or a bare ``export const x: T;``
 343    in a ``.d.ts``, is no entity."""
 344    if node.parent is not None and node.parent.type == "ambient_declaration":
 345        return True
 346    return any(
 347        _is_function_like(declarator.child_by_field_name("value"))
 348        for declarator in node.named_children
 349        if declarator.type == "variable_declarator"
 350    )
 351
 352
 353def _value_is_function(node: TreeSitterNode) -> bool:
 354    """A class field (``handler = () => {}``) or a default export of a function or class."""
 355    return _is_function_like(node.child_by_field_name("value"))
 356
 357
 358def _assigns_function(node: TreeSitterNode) -> bool:
 359    """An assignment of a function, as ``module.exports = function () {}`` is."""
 360    return _is_function_like(node.child_by_field_name("right"))
 361
 362
 363# The declarators a function declarator can be wrapped in: ``int *f();``,
 364# ``int &f();``, ``int f [[nodiscard]] ();``.
 365_CPP_DECLARATOR_WRAPPERS = {
 366    "pointer_declarator",
 367    "reference_declarator",
 368    "attributed_declarator",
 369}
 370
 371
 372def _cpp_operator_cast(node: TreeSitterNode) -> TreeSitterNode | None:
 373    """The ``operator_cast`` a conversion operator's declarator is, or ends in
 374    (``operator bool() const``, ``ns::W::operator bool() const``)."""
 375    declarator = node.child_by_field_name("declarator")
 376    while declarator is not None and declarator.type == "qualified_identifier":
 377        declarator = declarator.child_by_field_name("name")
 378    return (
 379        declarator
 380        if declarator is not None and declarator.type == "operator_cast"
 381        else None
 382    )
 383
 384
 385def _cpp_declares_function(node: TreeSitterNode) -> bool:
 386    """The declarator chain reaches a function declarator: ``int f(int);``, not the
 387    variable ``int x;`` nor the function pointer ``int (*fp)(int);``. A conversion
 388    operator (``operator bool() const;``) declares a function too."""
 389    if _cpp_operator_cast(node) is not None:
 390        return True
 391    declarator = node.child_by_field_name("declarator")
 392    for _ in range(8):
 393        if declarator is None:
 394            return False
 395        if declarator.type == "function_declarator":
 396            inner = declarator.child_by_field_name("declarator")
 397            return inner is not None and inner.type != "parenthesized_declarator"
 398        if declarator.type not in _CPP_DECLARATOR_WRAPPERS:
 399            return False
 400        declarator = declarator.child_by_field_name("declarator") or (
 401            declarator.named_children[-1] if declarator.named_children else None
 402        )
 403    return False
 404
 405
 406def in_function_body(node: TreeSitterNode) -> bool:
 407    """Whether the C or C++ ``node`` is inside a function's body (a compound statement,
 408    a lambda's included)."""
 409    parent = node.parent
 410    while parent is not None:
 411        if parent.type == "compound_statement":
 412            return True
 413        parent = parent.parent
 414    return False
 415
 416
 417def _cpp_prototype(node: TreeSitterNode) -> bool:
 418    """A function prototype outside a function body: inside one, ``SDLoc dl(Op);`` is a
 419    local variable that merely parses like a prototype."""
 420    return _cpp_declares_function(node) and not in_function_body(node)
 421
 422
 423def _cpp_has_body(node: TreeSitterNode) -> bool:
 424    """A class, struct, union or enum with a body: ``struct Foo;`` declares no entity."""
 425    return node.child_by_field_name("body") is not None
 426
 427
 428# Entity node types that are an entity only when their predicate holds, per language.
 429# Pester's blocks, lower-cased: PowerShell resolves command names in any case.
 430PESTER_BLOCKS = frozenset({"describe", "context", "it"})
 431PESTER_MODULE = "pester"
 432
 433
 434def _pester_block(node: TreeSitterNode) -> str | None:
 435    """The Pester block a command is (``describe``, ``context`` or ``it``, lower-cased),
 436    called bare or through Pester's module qualifier (``Pester\\It``); ``None`` for a
 437    command a script merely runs."""
 438    name = next(
 439        (child for child in node.named_children if child.type == "command_name"), None
 440    )
 441    if name is None or name.text is None:
 442        return None
 443    module, _, command = name.text.decode("utf-8").lower().rpartition("\\")
 444    return (
 445        command if command in PESTER_BLOCKS and module in ("", PESTER_MODULE) else None
 446    )
 447
 448
 449def _is_pester_block(node: TreeSitterNode) -> bool:
 450    """A command that is a Pester block (``Describe``, ``Context``, ``It``), called
 451    bare or through Pester's module qualifier (``Pester\\It``): a test, or a group of
 452    tests, rather than a command a script merely runs."""
 453    return _pester_block(node) is not None
 454
 455
 456ENTITY_PREDICATES: dict[CommentType, dict[str, Callable[[TreeSitterNode], bool]]] = {
 457    CommentType.cpp: {
 458        "declaration": _cpp_prototype,
 459        "field_declaration": _cpp_declares_function,
 460        "class_specifier": _cpp_has_body,
 461        "struct_specifier": _cpp_has_body,
 462        "union_specifier": _cpp_has_body,
 463        "enum_specifier": _cpp_has_body,
 464    },
 465    # so that a comment does not bind to a declaration that merely precedes the one
 466    # it documents (``const flag = true;`` before a function)
 467    CommentType.ts: {
 468        "lexical_declaration": _declares_function,
 469        "variable_declaration": _declares_function,
 470        "public_field_definition": _value_is_function,
 471        "assignment_expression": _assigns_function,
 472        "export_statement": _value_is_function,
 473    },
[docs] 474    # @Pester blocks are PowerShell entities, IMPL_PS_5, impl, [FE_POWERSHELL]
 475    CommentType.powershell: {"command": _is_pester_block},
 476}
 477SCOPE_PREDICATES = ENTITY_PREDICATES
 478"""The name this table had before entities were named; kept for importers."""
 479
 480COMMENT_TYPES = frozenset({"comment", "line_comment", "block_comment", "html_comment"})
 481"""The node kinds of a comment, in every grammar."""
 482
 483# What may stand between a leading marker and its entity, besides comments; a blank
 484# line has no node, so it is trivia in every language.
 485TRIVIA_TYPES: dict[CommentType, frozenset[str]] = {
 486    CommentType.rust: frozenset({"attribute_item", "inner_attribute_item"}),
 487    CommentType.ts: frozenset({"decorator"}),
 488    CommentType.cpp: frozenset({"attribute_declaration"}),
 489}
 490
 491# The node kinds that open an entity on a row before a comment -- an attribute, a
 492# decorator, a template header -- so a comment after one on its row is not trailing
 493# code: the item they belong to follows. An inner attribute (``#![...]``) applies to
 494# what it is inside, so it heads nothing.
 495ENTITY_HEAD_TYPES = frozenset(
 496    {
 497        "decorator",
 498        "template_parameter_list",
 499        "attribute_list",
 500        "attribute_declaration",
 501        "attribute_item",
 502    }
 503)
 504
 505# The node kinds an entity can be wrapped in, and where the wrapped node is: a field
 506# name, or ``None`` for the last named child that is no comment. The entity is the
 507# wrapped node, so a decorated or templated definition's lines exclude its decorators
 508# or its template header.
 509WRAPPER_TYPES: dict[CommentType, dict[str, str | None]] = {
 510    CommentType.cpp: {"template_declaration": None, "linkage_specification": "body"},
 511    CommentType.python: {"decorated_definition": "definition"},
 512    # an ``export_statement`` is an entity itself when it exports a function or class
 513    # value (``export default () => {}``)
 514    CommentType.ts: {
 515        "export_statement": "declaration",
 516        "ambient_declaration": None,
 517        "expression_statement": None,
 518    },
[docs] 519    # @PowerShell commands sit in a pipeline, IMPL_PS_8, impl, [FE_POWERSHELL]
 520    # every PowerShell command, a Pester block among them, sits in a pipeline (and a
 521    # pipeline chain), whatever path reaches it; see FIRST_ENTITY_WRAPPER_TYPES
 522    CommentType.powershell: {"pipeline": None, "pipeline_chain": None},
 523}
 524
 525# The wrappers that hold several items and are the first of them, in source order,
 526# that is an entity, rather than the item a field or their last child names: a
 527# PowerShell pipeline (``Describe 'x' { } | Out-Null`` is the Describe,
 528# ``Get-Thing | It 'y' { }`` the It) and a chain of pipelines (``&&``, ``||``).
 529FIRST_ENTITY_WRAPPER_TYPES: dict[CommentType, frozenset[str]] = {
 530    CommentType.powershell: frozenset({"pipeline", "pipeline_chain"}),
 531}
 532
[docs] 533# @VHDL wrappers a leading comment documents the first item of, IMPL_VHDL_5, impl, [FE_VHDL]
 534# The nodes whose FIRST item a comment before them documents, each with the child kinds
 535# passed over to reach it: VHDL's grammar places a comment before a design unit's
 536# context clauses beside the unit, a comment before the first declaration of a process,
 537# a block or a generate beside their head or body, and a comment before the first
 538# generic or port beside the interface list. Unlike a wrapper of WRAPPER_TYPES, such a
 539# node is no entity's stand-in: a comment inside one is not inside its first item, so
 540# only the next-entity search reads this table, and the first item must be an entity
 541# itself -- the search never walks past it to a later one.
 542FIRST_ITEM_WRAPPER_TYPES: dict[CommentType, dict[str, frozenset[str]]] = {
 543    CommentType.vhdl: {
 544        # a design unit's context clauses belong to the unit
 545        "design_unit": frozenset({"library_clause", "use_clause", "context_reference"}),
 546        "process_head": frozenset(),
 547        "block_head": frozenset(),
 548        "generate_head": frozenset(),
 549        "generate_block": frozenset(),
 550        "interface_list": frozenset(),
 551    },
 552}
 553
 554
 555def is_entity(node: TreeSitterNode, comment_type: CommentType) -> bool:
 556    """Whether ``node`` is an entity of its language (kind and predicate)."""
 557    if node.type not in ENTITY_NODE_TYPES.get(comment_type, ()):
 558        return False
 559    predicate = ENTITY_PREDICATES.get(comment_type, {}).get(node.type)
 560    return predicate is None or predicate(node)
 561
 562
 563def is_trivia(node: TreeSitterNode, comment_type: CommentType) -> bool:
 564    """Whether ``node`` may stand between a leading marker and its entity."""
 565    return node.type in COMMENT_TYPES or node.type in TRIVIA_TYPES.get(comment_type, ())
 566
 567
 568def is_wrapper(node: TreeSitterNode, comment_type: CommentType) -> bool:
 569    """Whether ``node`` can wrap an entity (see :data:`WRAPPER_TYPES`)."""
 570    return node.type in WRAPPER_TYPES.get(comment_type, {})
 571
 572
 573def unwrap_entity(
 574    node: TreeSitterNode | None, comment_type: CommentType
 575) -> TreeSitterNode | None:
 576    """The entity ``node`` is, or wraps (``@decorator``, ``template <…>``, ``export``,
 577    a PowerShell pipeline: see :data:`FIRST_ENTITY_WRAPPER_TYPES`)."""
 578    wrappers = WRAPPER_TYPES.get(comment_type, {})
 579    for _ in range(4):
 580        if node is None:
 581            return None
 582        if is_entity(node, comment_type):
 583            return node
 584        if node.type not in wrappers:
 585            return None
[docs] 586        # @A PowerShell pipeline is its first entity, IMPL_PS_17, impl, [FE_POWERSHELL]
 587        if node.type in FIRST_ENTITY_WRAPPER_TYPES.get(comment_type, ()):
 588            return next(
 589                (
 590                    entity
 591                    for child in node.named_children
 592                    if child.type not in COMMENT_TYPES
 593                    and (entity := unwrap_entity(child, comment_type)) is not None
 594                ),
 595                None,
 596            )
 597        field = wrappers[node.type]
 598        inner = node.child_by_field_name(field) if field is not None else None
 599        if inner is None:
 600            named = [c for c in node.named_children if c.type not in COMMENT_TYPES]
 601            inner = named[-1] if named else None
 602        node = inner
 603    return None
 604
 605
 606# ---------------------------------------------------------------------------
 607# Documentation comments
 608# ---------------------------------------------------------------------------
 609
 610# The openings of a documentation comment, per language. A line opening followed by
 611# one more ``/`` (``////``) or ``!`` (VHDL's ``--!!``, commented-out code) and a block
 612# opening followed by ``*`` or ``/`` (``/***``, ``/**/``) are plain comments.
 613_DOC_LINE_OPENINGS: dict[CommentType, tuple[str, ...]] = {
 614    CommentType.cpp: ("///", "//!"),
 615    CommentType.cs: ("///",),
 616    CommentType.rust: ("///", "//!"),
 617    # Doxygen's VHDL comment
 618    CommentType.vhdl: ("--!",),
 619}
 620_DOC_BLOCK_OPENINGS: dict[CommentType, tuple[str, ...]] = {
 621    CommentType.cpp: ("/**", "/*!"),
 622    CommentType.cs: ("/**",),
 623    CommentType.rust: ("/**", "/*!"),
 624    CommentType.ts: ("/**",),
 625}
 626
 627
 628def is_doc_comment(
 629    node: TreeSitterNode, comment_type: CommentType, *, text: bytes | None = None
 630) -> bool:
 631    """Whether the comment ``node`` is a documentation comment of its language.
 632
 633    A Python docstring is one too, but it is a string, not a comment: the caller knows
 634    it by its capture.
 635
 636    :param text: The comment's text as the file writes it (``SourceComment.text``);
 637        ``node.text`` when not given.
 638    """
 639    written = ((node.text or b"") if text is None else text).decode("utf-8", "replace")
 640    for opening in _DOC_LINE_OPENINGS.get(comment_type, ()):
 641        if written.startswith(opening):
 642            return not (
 643                (opening == "///" and written.startswith("////"))
 644                or (opening == "--!" and written.startswith("--!!"))
 645            )
 646    for opening in _DOC_BLOCK_OPENINGS.get(comment_type, ()):
 647        if written.startswith(opening):
 648            return opening == "/*!" or not written[len(opening) :].startswith(
 649                ("*", "/")
 650            )
 651    return False
 652
 653
 654# ---------------------------------------------------------------------------
 655# Naming: a definition node -> (kind, in-file qualified name, its components)
 656# ---------------------------------------------------------------------------
 657#
 658# Every namer builds the qualified name and its components (``semantic``) side by
 659# side, from the same walk; ``render_semantic`` spells the components back, and the
 660# truth test over the extraction fixtures holds the two together.
 661
 662
 663def _text(node: TreeSitterNode | None) -> str:
 664    if node is None:
 665        return ""
 666    return (node.text or b"").decode("utf-8", "replace")
 667
 668
 669def _is_ident_char(char: str) -> bool:
 670    return char.isalnum() or char == "_"
 671
 672
 673def _squeeze(text: str) -> str:
 674    """``text`` without whitespace, except one space between two identifier characters.
 675
 676    ``ns :: Widget`` becomes ``ns::Widget``, ``Box< T >`` becomes ``Box<T>``, and
 677    ``operator int`` keeps its space.
 678    """
 679    out: list[str] = []
 680    pending_space = False
 681    for char in text:
 682        if char.isspace():
 683            pending_space = True
 684            continue
 685        if pending_space and out and _is_ident_char(out[-1]) and _is_ident_char(char):
 686            out.append(" ")
 687        pending_space = False
 688        out.append(char)
 689    return "".join(out)
 690
 691
 692def _collapse(text: str) -> str:
 693    """``text`` with every whitespace run collapsed to one space, and trimmed."""
 694    return re.sub(r"\s+", " ", text).strip()
 695
 696
 697def _strip_template_args(text: str) -> str:
 698    """``text`` without its ``<…>`` argument lists (``operator<`` is kept)."""
 699    out: list[str] = []
 700    depth = 0
 701    for char in text:
 702        so_far = "".join(out)
 703        if char == "<" and depth == 0 and so_far.endswith(("operator", "operator<")):
 704            out.append(char)
 705        elif char == "<":
 706            depth += 1
 707        elif char == ">" and depth:
 708            depth -= 1
 709        elif depth == 0:
 710            out.append(char)
 711    return "".join(out)
 712
 713
 714def _join(separator: str, segments: list[str], own: str | None) -> str | None:
 715    if own is None:
 716        return None
 717    return separator.join([*segments, own])
 718
 719
 720def _element(kind: str, name: str) -> SemanticElement:
 721    return {"kind": kind, "name": name}
 722
 723
 724def _named(
 725    kind: EntityKind | None,
 726    qualified_name: str | None,
 727    semantic: Iterable[SemanticElement],
 728    name_end: int | None = None,
 729) -> EntityName:
 730    """An entity's name and its components; no components when it has no name."""
 731    if qualified_name is None:
 732        return EntityName(kind, None, None, name_end)
 733    return EntityName(kind, qualified_name, tuple(semantic), name_end)
 734
 735
 736# --- C and C++ ---------------------------------------------------------------
 737
 738_CPP_CLASS_KINDS: dict[str, EntityKind] = {
 739    "class_specifier": "class",
 740    "struct_specifier": "struct",
 741    "union_specifier": "union",
 742    "enum_specifier": "enum",
 743}
 744
 745# The kinds the namer gives a function only when a class qualifies it.
 746_CPP_MEMBER_KINDS = frozenset({"method", "constructor", "destructor"})
 747
 748
 749def _cpp_text(node: TreeSitterNode | None, source: bytes | None) -> str:
 750    """The text of ``node``, read from ``source`` when given.
 751
 752    ``source`` is the file as written: a C++ tree may be parsed from a prepared copy of
 753    it (:mod:`sphinx_codelinks.analyse.cpp_macros`), and every name is read from the
 754    file, never from that copy. Without it, the tree's own text is read.
 755    """
 756    return _cpp_bytes(node, source).decode("utf-8", "replace")
 757
 758
 759def _cpp_bytes(node: TreeSitterNode | None, source: bytes | None) -> bytes:
 760    """The bytes of ``node``, read from ``source`` when given (see :func:`_cpp_text`)."""
 761    if node is None:
 762        return b""
 763    if source is None:
 764        return node.text or b""
 765    return source[node.start_byte : node.end_byte]
 766
 767
 768@dataclass(frozen=True)
 769class _CppNaming:
 770    """How the names of a C++ tree's entities are made beyond one node."""
 771
 772    full_chain: bool = False
 773    """Keep every container, an inline namespace and a friend's classes included, as
 774    the names were made before they were left out (see :func:`cpp_renames`)."""
 775    specs: _PartialSpecs | None = None
 776    """The file's partial specialisations, which an out-of-line qualifier is spelled
 777    as (:func:`_cpp_name`)."""
 778    inline: frozenset[str] | None = None
 779    """The paths of the file's inline namespaces as the full chain spells them
 780    (``lib::v2``), which a written qualifier leaves out
 781    (:func:`_cpp_without_inline_parts`)."""
 782    macros: Callable[[int], str] | None = None
 783    """The namespaces the file's own macros open around a name ending at a byte, each
 784    followed by ``::``."""
 785
 786    def macros_at(self, name_end: int) -> str:
 787        """The namespaces the file's own macros open around a name ending at
 788        ``name_end`` (``ns::`` or empty)."""
 789        return self.macros(name_end) if self.macros is not None else ""
 790
 791    def specs_at(
 792        self, node: TreeSitterNode, source: bytes | None, name_end: int, written: str
 793    ) -> _Lookup | None:
 794        """Where the templates the name ``written`` at ``node`` (ending at
 795        ``name_end``) qualifies are looked up among the file's partial specialisations
 796        (:class:`_Lookup`), when it has any."""
 797        if self.specs is None:
 798            return None
 799        macros = self.macros_at(name_end)
 800        inline_parts = (
 801            _cpp_inline_segments(
 802                self.inline, macros, node, source, _squeeze(written).lstrip(":")
 803            )
 804            if self.inline is not None
 805            else []
 806        )
 807        return _Lookup(
 808            self.specs, _cpp_scope_of(node, source, self, macros), inline_parts
 809        )
 810
 811
 812@dataclass(frozen=True)
 813class _Lookup:
 814    """Where the templates an out-of-line qualifier names are looked up among the
 815    file's partial specialisations (:func:`_cpp_name`)."""
 816
 817    specs: _PartialSpecs
 818    """The file's partial specialisations."""
 819    scope: str
 820    """Where the name is written (:func:`_cpp_scope_of`)."""
 821    inline_parts: list[int]
 822    """The parts of the written name that name an inline namespace of the file
 823    (:func:`_cpp_inline_segments`): left out of the path, as the specialisations' own
 824    keys leave them out."""
 825
 826
 827_ALONE = _CppNaming()
 828_FULL = _CppNaming(full_chain=True)
 829
 830
 831def _cpp_templated(node: TreeSitterNode) -> bool:
 832    """Under a ``template`` with parameters (not an explicit specialisation)."""
 833    parent = node.parent
 834    while parent is not None and parent.type == "template_declaration":
 835        params = parent.child_by_field_name("parameters")
 836        if params is not None and params.named_child_count > 0:
 837            return True
 838        parent = parent.parent
 839    return False
 840
 841
 842def _cpp_template_headers(
 843    node: TreeSitterNode, source: bytes | None
 844) -> list[list[str]]:
 845    """The parameter names of each ``template <…>`` header around ``node``, outermost
 846    first (``template <typename T> template <typename U>`` is ``[T]``, ``[U]``;
 847    ``template <>`` is ``[]``; a pack ``typename... Ts`` is ``Ts``)."""
 848    headers: list[list[str]] = []
 849    parent = node.parent
 850    while parent is not None and parent.type == "template_declaration":
 851        parameters = parent.child_by_field_name("parameters")
 852        headers.append(
 853            [
 854                _cpp_parameter_name(parameter, source)
 855                for parameter in parameters.named_children
 856                if parameter.type != "comment"
 857            ]
 858            if parameters is not None
 859            else []
 860        )
 861        parent = parent.parent
 862    headers.reverse()
 863    return headers
 864
 865
 866def _cpp_parameter_name(parameter: TreeSitterNode, source: bytes | None) -> str:
 867    """The name a template parameter declares (``T`` of ``typename T = int``, ``N`` of
 868    ``int N``, ``Ts`` of ``typename... Ts``, ``TT`` of
 869    ``template <class> class TT``); empty for an unnamed one."""
 870    if parameter.type in (
 871        "type_parameter_declaration",
 872        "variadic_type_parameter_declaration",
 873    ):
 874        name = next(
 875            (c for c in parameter.named_children if c.type == "type_identifier"), None
 876        )
 877    elif parameter.type == "optional_type_parameter_declaration":
 878        name = parameter.child_by_field_name("name")
 879    elif parameter.type == "template_template_parameter_declaration":
 880        inner = next(
 881            (
 882                c
 883                for c in reversed(parameter.named_children)
 884                if c.type.endswith("type_parameter_declaration")
 885            ),
 886            None,
 887        )
 888        return _cpp_parameter_name(inner, source) if inner is not None else ""
 889    else:
 890        # a non-type parameter: the identifier its declarator ends in
 891        declarator = parameter.child_by_field_name("declarator")
 892        stack = [declarator] if declarator is not None else []
 893        name = None
 894        while stack:
 895            current = stack.pop()
 896            if current.type == "identifier":
 897                name = current
 898                break
 899            stack.extend(reversed(current.named_children))
 900    return _squeeze(_cpp_text(name, source))
 901
 902
 903def _cpp_written_arguments(part: str) -> list[str] | None:
 904    """The arguments of ``part``'s template argument list (``P<T,4>`` is ``T`` and
 905    ``4``), split at its top-level commas, a pack expansion with its ``...``
 906    (``Ts...``). ``None`` when ``part`` has no argument list."""
 907    open_at = part.find("<")
 908    if open_at < 0 or not part.endswith(">") or open_at + 1 > len(part) - 1:
 909        return None
 910    inner = part[open_at + 1 : -1]
 911    arguments: list[str] = []
 912    depth = start = 0
 913    for index, char in enumerate(inner):
 914        if char in "<([":
 915            depth += 1
 916        elif char in ">)]":
 917            depth = max(depth - 1, 0)
 918        elif char == "," and depth == 0:
 919            arguments.append(inner[start:index])
 920            start = index + 1
 921    arguments.append(inner[start:])
 922    return [argument.strip() for argument in arguments]
 923
 924
 925def _cpp_template_arguments(part: str) -> list[str] | None:
 926    """:func:`_cpp_written_arguments`, a pack expansion without its ``...`` (``Ts...``
 927    is ``Ts``): what a header's parameter names are compared with."""
 928    written = _cpp_written_arguments(part)
 929    if written is None:
 930        return None
 931    stripped = []
 932    for argument in written:
 933        while argument.endswith("..."):
 934            argument = argument[: -len("...")]
 935        stripped.append(argument)
 936    return stripped
 937
 938
 939def _cpp_words(text: str) -> list[str]:
 940    """The identifiers of ``text``, in order."""
 941    words: list[str] = []
 942    start: int | None = None
 943    for index, char in enumerate(text):
 944        if _is_ident_char(char):
 945            if start is None:
 946                start = index
 947        elif start is not None:
 948            words.append(text[start:index])
 949            start = None
 950    if start is not None:
 951        words.append(text[start:])
 952    return words
 953
 954
 955def _cpp_canonical(arguments: list[str], parameters: list[str]) -> list[str]:
 956    """``arguments`` with every name in ``parameters`` replaced by its place among the
 957    parameters named so far (``$0``, ``$1``, …): two argument lists that are the same
 958    but for their own template parameter names (``<T,4>`` under
 959    ``template <typename T>``, ``<U,4>`` under ``template <typename U>``) give the same
 960    list."""
 961    seen: list[str] = []
 962
 963    def spelled(word: str) -> str:
 964        if word not in parameters:
 965            return word
 966        if word not in seen:
 967            seen.append(word)
 968        return f"${seen.index(word)}"
 969
 970    canonical: list[str] = []
 971    for argument in arguments:
 972        out: list[str] = []
 973        start: int | None = None
 974        for index, char in enumerate(argument):
 975            if _is_ident_char(char):
 976                if start is None:
 977                    start = index
 978                continue
 979            if start is not None:
 980                out.append(spelled(argument[start:index]))
 981                start = None
 982            out.append(char)
 983        if start is not None:
 984            out.append(spelled(argument[start:]))
 985        canonical.append("".join(out))
 986    return canonical
 987
 988
 989@dataclass(frozen=True)
 990class _PartialSpec:
 991    """One partial specialisation :class:`_PartialSpecs` holds."""
 992
 993    spelled: str
 994    """Its last name part, squeezed (``P<T,4>``)."""
 995    canonical: list[str]
 996    """Its arguments up to its own parameter names (:func:`_cpp_canonical`)."""
 997    defined: bool
 998    """Whether it has a body: a forward declaration does not."""
 999
1000
1001class _PartialSpecs:
1002    """The partial specialisations of class templates a C++ file declares, by the
1003    template's qualified name (:func:`_cpp_scope_of` and its name: ``ns1::P``, so
1004    ``ns1::P<T,4>`` and ``ns2::P<U,4>`` never share one): each as it spells its last
1005    name part (``P<T,4>``), its arguments up to its own parameter names, a pack's
1006    ``...`` kept (:func:`_cpp_canonical`), and whether it has a body. An out-of-line
1007    qualifier the same but for its parameter names as exactly one spelling of those
1008    with a body is spelled as it is (:func:`_cpp_name`), so the members the
1009    specialisation defines join their in-class declarations; a forward declaration
1010    never sets the spelling, and where two spellings remain none does."""
1011
1012    def __init__(self) -> None:
1013        self.by_template: dict[str, list[_PartialSpec]] = {}
1014
1015    @classmethod
1016    def of(
1017        cls, nodes: list[TreeSitterNode], source: bytes | None, naming: _CppNaming
1018    ) -> _PartialSpecs:
1019        """The partial specialisations among ``nodes`` (:meth:`add`)."""
1020        specs = cls()
1021        for node in nodes:
1022            specs.add(node, source, naming)
1023        return specs
1024
1025    def add(
1026        self, node: TreeSitterNode, source: bytes | None, naming: _CppNaming
1027    ) -> None:
1028        """``node``, when it is a class, struct or union named with template arguments
1029        under a header with parameters (a partial specialisation), keyed as ``naming``
1030        reads where it is (the file's own macros and its inline namespaces,
1031        :func:`_cpp_scope_of`)."""
1032        if not _cpp_templated_class(node):
1033            return
1034        name = node.child_by_field_name("name")
1035        if name is None:
1036            return
1037        headers = _cpp_template_headers(node, source)
1038        if not headers or not headers[-1]:
1039            return
1040        spelled = _cpp_class_name(name, node, source)
1041        *qualifiers, part = _cpp_split(spelled)
1042        arguments = _cpp_written_arguments(part)
1043        if arguments is None:
1044            return
1045        macros = naming.macros_at(name.end_byte)
1046        inline_parts = (
1047            _cpp_inline_segments(naming.inline, macros, node, source, spelled)
1048            if naming.inline is not None
1049            else []
1050        )
1051        template = _cpp_scope_of(node, source, naming, macros)
1052        template += "".join(
1053            f"{_strip_template_args(qualifier)}::"
1054            for index, qualifier in enumerate(qualifiers)
1055            if index not in inline_parts
1056        )
1057        template += part.split("<", 1)[0]
1058        self.by_template.setdefault(template, []).append(
1059            _PartialSpec(
1060                part,
1061                _cpp_canonical(arguments, headers[-1]),
1062                node.child_by_field_name("body") is not None,
1063            )
1064        )
1065
1066    def spelling(self, scope: str, part: str, parameters: list[str]) -> str | None:
1067        """How the partial specialisations of the file spell ``part`` (a qualifier in
1068        ``scope``, :func:`_cpp_scope_of` and the qualifiers before it, under a header
1069        of ``parameters``): the one spelling of those with a body that are the same but
1070        for their parameter names, when there is exactly one."""
1071        arguments = _cpp_written_arguments(part)
1072        if arguments is None:
1073            return None
1074        wanted = _cpp_canonical(arguments, parameters)
1075        spellings = {
1076            spec.spelled
1077            for spec in self.by_template.get(scope + part.split("<", 1)[0], ())
1078            if spec.defined and spec.canonical == wanted
1079        }
1080        return next(iter(spellings)) if len(spellings) == 1 else None
1081
1082
1083def _cpp_templated_class(node: TreeSitterNode) -> bool:
1084    """``node`` is a class, struct or union under a template header: a partial
1085    specialisation when it is named with arguments under a header with parameters
1086    (:meth:`_PartialSpecs.add`)."""
1087    return (
1088        node.type in ("class_specifier", "struct_specifier", "union_specifier")
1089        and node.parent is not None
1090        and node.parent.type == "template_declaration"
1091    )
1092
1093
1094def _cpp_scope_of(
1095    node: TreeSitterNode, source: bytes | None, naming: _CppNaming, macros: str
1096) -> str:
1097    """Where the templates a name written at ``node`` qualifies are looked up:
1098    ``macros`` (the namespaces the file's own macros open around it, ``ns::`` or
1099    empty), then the namespaces and classes around it as ``naming`` names them with
1100    every inline namespace left out (:func:`_cpp_containers`), each without its
1101    arguments and followed by ``::``."""
1102    around = replace(naming, full_chain=False, specs=None)
1103    segments, _ = _cpp_containers(node, source, around)
1104    return macros + "".join(
1105        f"{_strip_template_args(part)}::"
1106        for segment in segments
1107        for part in _cpp_split(segment)
1108    )
1109
1110
1111def _cpp_name(
1112    text: str,
1113    headers: list[list[str]],
1114    *,
1115    strip_last: bool,
1116    lookup: _Lookup | None,
1117) -> str:
1118    """A name as the naming spells it: ``text`` squeezed, without a leading ``::``.
1119
1120    A qualifier's template argument list that names a parameter of the next
1121    ``template <…>`` header around the definition (``headers``, outermost first) is
1122    matched with that header; one that names none of them (an explicit specialisation,
1123    ``Outer<int>``) is kept and takes no header but an empty ``template <>``
1124    (``template <> template <class U> void Outer<int>::Inner<U>::n()`` matches
1125    ``Inner<U>`` with ``<class U>``). A matched list is spelled as a partial
1126    specialisation of the file spells its arguments when the two are the same but for
1127    their own parameter names (``lookup``, with the scope the name is written in,
1128    ``ns::`` or empty: ``template <typename U> void P<U,4>::fill()`` is ``P<T,4>::fill``
1129    when the file defines ``template <typename T> struct P<T,4> {…}``;
1130    :class:`_PartialSpecs`, :class:`_Lookup`).
1131    Otherwise it is the template's own when its arguments are exactly the header's
1132    parameter names (the primary template's member,
1133    ``template <typename T> void Box<T>::get()``), named without it (``Box::get``), and
1134    a specialisation's, kept, when they are not (``template <typename B> void
1135    W<B,4>::fill()`` is ``W<B,4>::fill``). The last part's list is stripped when
1136    ``strip_last`` (a function under a header with parameters).
1137    """
1138    parts = _cpp_split(_squeeze(text).lstrip(":"))
1139    last = len(parts) - 1
1140    remaining = iter(headers)
1141    upcoming = next(remaining, None)
1142    # where the template a qualifier names is: the scope, then the qualifiers before
1143    # it, without their arguments and without those that name an inline namespace of
1144    # the file
1145    path = lookup.scope if lookup is not None else ""
1146    spelled: list[str] = []
1147    for index, part in enumerate(parts):
1148        if index == last:
1149            spelled.append(_strip_template_args(part) if strip_last else part)
1150            break
1151        arguments = _cpp_template_arguments(part)
1152        if arguments is None:
1153            spelled.append(part)
1154        elif upcoming is None or not any(
1155            word in upcoming for argument in arguments for word in _cpp_words(argument)
1156        ):
1157            # an explicit specialisation's empty ``template <>`` is its own
1158            if upcoming == []:
1159                upcoming = next(remaining, None)
1160            spelled.append(part)
1161        else:
1162            parameters = upcoming
1163            upcoming = next(remaining, None)
1164            as_spec = (
1165                lookup.specs.spelling(path, part, parameters)
1166                if lookup is not None
1167                else None
1168            )
1169            if as_spec is not None:
1170                spelled.append(as_spec)
1171            elif parameters == arguments:
1172                spelled.append(_strip_template_args(part))
1173            else:
1174                spelled.append(part)
1175        if lookup is None or index not in lookup.inline_parts:
1176            path += f"{_strip_template_args(part)}::"
1177    return "::".join(spelled)
1178
1179
1180def _cpp_text_without_comments(node: TreeSitterNode, source: bytes | None) -> str:
1181    """The text of ``node`` with every comment inside it left out (``json.hpp`` writes
1182    ``//`` comments between a specialisation's template arguments)."""
1183    comments: list[tuple[int, int]] = []
1184    stack = [node]
1185    while stack:
1186        current = stack.pop()
1187        if current.type == "comment":
1188            comments.append((current.start_byte, current.end_byte))
1189        else:
1190            stack.extend(current.children)
1191    written = _cpp_bytes(node, source)
1192    if not comments:
1193        return written.decode("utf-8", "replace")
1194    out: list[bytes] = []
1195    position = node.start_byte
1196    for start, end in sorted(comments):
1197        out.append(written[position - node.start_byte : start - node.start_byte])
1198        out.append(b" ")
1199        position = end
1200    out.append(written[position - node.start_byte :])
1201    return b"".join(out).decode("utf-8", "replace")
1202
1203
1204def _cpp_class_name(
1205    name: TreeSitterNode, class_node: TreeSitterNode, source: bytes | None
1206) -> str:
1207    """A class's name: a specialisation's arguments kept, squeezed and without comments
1208    (``P<T,4>``, explicit or partial), as clang names it; a primary template's name has
1209    none to strip."""
1210    return _cpp_name(
1211        _cpp_text_without_comments(name, source),
1212        _cpp_template_headers(class_node, source),
1213        strip_last=False,
1214        lookup=None,
1215    )
1216
1217
1218def _cpp_class_name_in(
1219    name: TreeSitterNode,
1220    class_node: TreeSitterNode,
1221    source: bytes | None,
1222    naming: _CppNaming,
1223) -> str:
1224    """:func:`_cpp_class_name`, without the parts of a written qualifier that name an
1225    inline namespace of the file (``struct lib::v2::X {…}`` is ``lib::X``, so its
1226    members are named in it), unless ``naming`` keeps every container."""
1227    own = _cpp_class_name(name, class_node, source)
1228    if naming.inline is None or naming.full_chain:
1229        return own
1230    dropped = _cpp_inline_segments(
1231        naming.inline, naming.macros_at(name.end_byte), class_node, source, own
1232    )
1233    return _cpp_without_parts(own, dropped) if dropped else own
1234
1235
1236def _cpp_namespace_name(node: TreeSitterNode, source: bytes | None) -> str:
1237    name = node.child_by_field_name("name")
1238    if name is None:
1239        return "(anonymous namespace)"
1240    return _squeeze(_cpp_text(name, source))
1241
1242
1243def _cpp_is_inline_namespace(node: TreeSitterNode) -> bool:
1244    """The namespace ``node`` is declared ``inline`` (``inline namespace v1``)."""
1245    for child in node.children:
1246        if child.type == "namespace":
1247            return False
1248        if child.type == "inline":
1249            return True
1250    return False
1251
1252
1253def _cpp_inline_namespace_or_part(node: TreeSitterNode) -> bool:
1254    """The namespace ``node`` is inline, or holds an inline part in its nested name
1255    (``namespace a::inline b``): an ``inline`` token, as
1256    :func:`_cpp_is_inline_namespace` reads one (``namespace pipeline`` has none)."""
1257    if _cpp_is_inline_namespace(node):
1258        return True
1259    name = node.child_by_field_name("name")
1260    return name is not None and _cpp_has_inline_part(name)
1261
1262
1263def _cpp_has_inline_part(name: TreeSitterNode) -> bool:
1264    """The nested namespace specifier ``name`` holds an ``inline`` token."""
1265    if name.type != "nested_namespace_specifier":
1266        return False
1267    return any(
1268        child.type == "inline" or _cpp_has_inline_part(child) for child in name.children
1269    )
1270
1271
1272def _cpp_namespaces_around(node: TreeSitterNode, source: bytes | None) -> str:
1273    """The namespaces around ``node`` with every part kept (an inline one too),
1274    outermost first, each followed by ``::`` (``lib::v1::``)."""
1275    groups: list[list[str]] = []
1276    parent = node.parent
1277    while parent is not None:
1278        if parent.type == "namespace_definition":
1279            groups.append(_cpp_namespace_parts(parent, source, own=False, naming=_FULL))
1280        parent = parent.parent
1281    return "".join(f"{part}::" for group in reversed(groups) for part in group)
1282
1283
1284def _cpp_inline_paths_of(
1285    node: TreeSitterNode, source: bytes | None, prefix: Callable[[int], str]
1286) -> list[str]:
1287    """The paths of the inline parts of the namespace ``node`` (an inline namespace, or
1288    one with an inline part in its nested name), as the full chain spells them, after
1289    the namespaces the file's own macros open (``lib::v2``); none for an unnamed one,
1290    which no qualifier can name."""
1291    name = node.child_by_field_name("name")
1292    if name is None:
1293        return []
1294    parts = _cpp_split(_cpp_namespace_name(node, source))
1295    last = len(parts) - 1
1296    path = prefix(name.end_byte) + _cpp_namespaces_around(node, source)
1297    paths: list[str] = []
1298    for index, part in enumerate(parts):
1299        inline = part.startswith("inline ")
1300        bare = part.removeprefix("inline ")
1301        if not inline:
1302            inline = index == last and _cpp_is_inline_namespace(node)
1303        path += bare
1304        if inline:
1305            paths.append(path)
1306        path += "::"
1307    return paths
1308
1309
1310def _cpp_without_inline_parts(
1311    naming: _CppNaming,
1312    node: TreeSitterNode,
1313    source: bytes | None,
1314    own: str,
1315    name_end: int,
1316    semantic: list[SemanticElement],
1317) -> tuple[str, list[SemanticElement]]:
1318    """``own``, the name written at ``node`` (its name ending at ``name_end``), and its
1319    components ``semantic`` (which end with ``own``'s), without the parts of its
1320    qualifier that name an inline namespace of the file, as the namespaces around a
1321    node leave one out (``void lib::v2::g() {}`` is ``lib::g`` when the file declares
1322    ``inline namespace v2`` in ``lib``); unchanged with every container kept."""
1323    if naming.inline is None or naming.full_chain:
1324        return own, semantic
1325    dropped = _cpp_inline_segments(
1326        naming.inline, naming.macros_at(name_end), node, source, own
1327    )
1328    if not dropped:
1329        return own, semantic
1330    first = max(len(semantic) - len(_cpp_split(own)), 0)
1331    kept_semantic = [
1332        element
1333        for position, element in enumerate(semantic)
1334        if position - first not in dropped
1335    ]
1336    return _cpp_without_parts(own, dropped), kept_semantic
1337
1338
1339def _cpp_inline_segments(
1340    paths: frozenset[str],
1341    macros: str,
1342    node: TreeSitterNode,
1343    source: bytes | None,
1344    own: str,
1345) -> list[int]:
1346    """The parts of ``own``, the name written at ``node``, that name one of the file's
1347    inline namespaces (``paths``): indexes into its qualifier's parts, read from
1348    ``macros`` (the namespaces the file's own macros open around it) and the
1349    namespaces around ``node`` (``void lib::v2::g() {}`` is ``lib``, ``v2``, ``g``, and
1350    ``v2`` is one when the file declares ``inline namespace v2`` in ``lib``)."""
1351    *qualifier, _ = _cpp_split(own)
1352    path = macros + _cpp_namespaces_around(node, source)
1353    dropped: list[int] = []
1354    for index, part in enumerate(qualifier):
1355        path += part
1356        if path in paths:
1357            dropped.append(index)
1358        path += "::"
1359    return dropped
1360
1361
1362def _cpp_without_parts(name: str, dropped: list[int]) -> str:
1363    """``name`` without the parts at ``dropped``."""
1364    return "::".join(
1365        part for index, part in enumerate(_cpp_split(name)) if index not in dropped
1366    )
1367
1368
1369def _cpp_namespace_parts(
1370    node: TreeSitterNode, source: bytes | None, *, own: bool, naming: _CppNaming
1371) -> list[str]:
1372    """The parts of the namespace ``node``'s name (``namespace a::b`` is ``a`` and
1373    ``b``) that a name qualified by it keeps.
1374
1375    None for an inline namespace, and none for an inline part of a nested name
1376    (``a::inline b`` keeps ``a``), as clang prints a name: ``nlohmann::basic_json``,
1377    not ``nlohmann::json_abi_v3_12_0::basic_json`` (the ABI tag changes with every
1378    release). As the entity itself (``own``), its own last part is kept, without the
1379    ``inline`` (``a::inline b`` is ``a`` and ``b``). A part that reopens one of the
1380    file's inline namespaces without ``inline`` is inline too, as C++ makes it
1381    (:func:`_cpp_reopened_parts`, when ``naming`` knows them). With
1382    ``naming.full_chain``, every part is kept, an inline one without its ``inline``.
1383    """
1384    reopened = [] if naming.full_chain else _cpp_reopened_parts(node, source, naming)
1385    parts = _cpp_split(_cpp_namespace_name(node, source))
1386    last = len(parts) - 1
1387    kept: list[str] = []
1388    for index, part in enumerate(parts):
1389        inline = part.startswith("inline ") or (
1390            index == last and _cpp_is_inline_namespace(node)
1391        )
1392        if (
1393            not (inline or index in reopened)
1394            or naming.full_chain
1395            or (own and index == last)
1396        ):
1397            kept.append(part.removeprefix("inline "))
1398    return kept
1399
1400
1401def _cpp_reopened_parts(
1402    node: TreeSitterNode, source: bytes | None, naming: _CppNaming
1403) -> list[int]:
1404    """The parts of the namespace ``node``'s name that reopen one of the file's inline
1405    namespaces without ``inline`` (``namespace lib { namespace v1 {…} }`` after
1406    ``inline namespace v1`` in ``lib``): their paths, read as the file's inline paths
1407    are, are among ``naming``'s; none when it has none."""
1408    name = node.child_by_field_name("name")
1409    if naming.inline is None or name is None:
1410        return []
1411    path = naming.macros_at(name.end_byte) + _cpp_namespaces_around(node, source)
1412    reopened: list[int] = []
1413    for index, part in enumerate(_cpp_split(_cpp_namespace_name(node, source))):
1414        path += part.removeprefix("inline ")
1415        if path in naming.inline:
1416            reopened.append(index)
1417        path += "::"
1418    return reopened
1419
1420
1421def _cpp_reopens_inline(
1422    node: TreeSitterNode,
1423    source: bytes | None,
1424    prefix: Callable[[int], str],
1425    paths: set[str],
1426) -> bool:
1427    """The namespace ``node`` reopens one of the file's inline namespaces met so far
1428    without ``inline`` (a part's path is among ``paths``), which C++ makes inline too:
1429    the first declaration is the inline one."""
1430    if not paths:
1431        return False
1432    naming = _CppNaming(inline=frozenset(paths), macros=prefix)
1433    return bool(_cpp_reopened_parts(node, source, naming))
1434
1435
1436def _cpp_operator_at(text: str, index: int) -> bool:
1437    """``text`` has the word ``operator`` at ``index``."""
1438    end = index + len("operator")
1439    return text.startswith("operator", index) and (
1440        end == len(text) or not _is_ident_char(text[end])
1441    )
1442
1443
1444def _cpp_split(text: str) -> list[str]:
1445    """The components of a name as written, at its ``::``: none inside ``<…>`` or
1446    ``(…)``, and none after ``operator`` (``W::operator std::string`` is ``W`` and
1447    ``operator std::string``; ``Box<std::string>::get`` is ``Box<std::string>`` and
1448    ``get``)."""
1449    parts: list[str] = []
1450    start = depth = index = 0
1451    while index < len(text) and not (index == start and _cpp_operator_at(text, index)):
1452        char = text[index]
1453        if char in "<(":
1454            depth += 1
1455        elif char in ">)" and depth:
1456            depth -= 1
1457        elif depth == 0 and text.startswith("::", index):
1458            parts.append(text[start:index])
1459            index += 2
1460            start = index
1461            continue
1462        index += 1
1463    parts.append(text[start:])
1464    return parts
1465
1466
1467def _cpp_qualified(text: str, kind: str, *, member: bool) -> list[SemanticElement]:
1468    """The components of a name written qualified (``ns::W::f``), the last one of
1469    ``kind``. What a qualifier is, the syntax cannot tell: each is a ``scope``, except
1470    the one right before a ``member`` (a method, constructor, destructor or conversion
1471    operator), which is its ``class``."""
1472    *qualifiers, own = _cpp_split(text)
1473    elements = [_element("scope", qualifier) for qualifier in qualifiers]
1474    if member and qualifiers:
1475        elements[-1] = _element("class", qualifiers[-1])
1476    return [*elements, _element(kind, own)]
1477
1478
1479def _cpp_is_friend(node: TreeSitterNode) -> bool:
1480    """``node`` is what a ``friend`` declaration declares or defines
1481    (``friend bool operator==(const A&, const A&) {…}``, a friend template's too): a
1482    function of the innermost enclosing namespace, not of the class."""
1483    return node.parent is not None and node.parent.type == "friend_declaration"
1484
1485
1486def _cpp_containers(
1487    node: TreeSitterNode, source: bytes | None, naming: _CppNaming
1488) -> tuple[list[str], list[SemanticElement]]:
1489    """The namespaces and classes around ``node``, outermost first: their names, and
1490    their components (``namespace a::b`` is two namespaces). An inline namespace adds
1491    nothing (:func:`_cpp_namespace_parts`), and a function a ``friend`` declaration
1492    declares or defines is named in its innermost enclosing namespace, as clang names
1493    it: the classes around it add nothing."""
1494    friend = not naming.full_chain and _cpp_is_friend(node)
1495    segments: list[str] = []
1496    groups: list[list[SemanticElement]] = []
1497    parent = node.parent
1498    while parent is not None:
1499        if parent.type == "namespace_definition":
1500            parts = _cpp_namespace_parts(parent, source, own=False, naming=naming)
1501            if parts:
1502                segments.append("::".join(parts))
1503                groups.append([_element("namespace", part) for part in parts])
1504        elif not friend and parent.type in (
1505            "class_specifier",
1506            "struct_specifier",
1507            "union_specifier",
1508        ):
1509            name = parent.child_by_field_name("name")
1510            if name is not None:
1511                container = _cpp_class_name_in(name, parent, source, naming)
1512                segments.append(container)
1513                groups.append(
1514                    _cpp_qualified(
1515                        container, _CPP_CLASS_KINDS[parent.type], member=False
1516                    )
1517                )
1518        parent = parent.parent
1519    return (
1520        list(reversed(segments)),
1521        [element for group in reversed(groups) for element in group],
1522    )
1523
1524
1525def cpp_function_declarator(node: TreeSitterNode) -> TreeSitterNode | None:
1526    """The ``function_declarator`` of a function definition or declaration."""
1527    declarator = node.child_by_field_name("declarator")
1528    for _ in range(8):
1529        if declarator is None:
1530            return None
1531        if declarator.type == "function_declarator":
1532            return declarator
1533        declarator = declarator.child_by_field_name("declarator") or (
1534            declarator.named_children[-1] if declarator.named_children else None
1535        )
1536    return None
1537
1538
1539# What may stand between a class member and its class body: a member template's
1540# header, a preprocessor conditional.
1541_CPP_MEMBER_WRAPPERS = frozenset(
1542    {
1543        "template_declaration",
1544        "preproc_if",
1545        "preproc_ifdef",
1546        "preproc_elif",
1547        "preproc_elifdef",
1548        "preproc_else",
1549    }
1550)
1551
1552
1553def _cpp_in_class(node: TreeSitterNode) -> bool:
1554    """A direct member of a class body (a member template, or a member inside
1555    ``#if`` / ``#else``, counts)."""
1556    parent = node.parent
1557    while parent is not None and parent.type in _CPP_MEMBER_WRAPPERS:
1558        parent = parent.parent
1559    return parent is not None and parent.type == "field_declaration_list"
1560
1561
1562def _cpp_defined_by_macro(node: TreeSitterNode) -> bool:
1563    """A function a macro defines (``TEST(Suite, Name) {...}``): a definition with no
1564    return type, outside a class, whose declarator is a plain identifier. The function
1565    the compiler sees is the macro's expansion, which the syntax cannot name."""
1566    if node.type != "function_definition" or node.child_by_field_name("type"):
1567        return False
1568    if _cpp_in_class(node):
1569        return False
1570    declarator = cpp_function_declarator(node)
1571    inner = (
1572        declarator.child_by_field_name("declarator") if declarator is not None else None
1573    )
1574    return inner is not None and inner.type == "identifier"
1575
1576
1577def _cpp_conversion_parameters(cast: TreeSitterNode) -> TreeSitterNode | None:
1578    """A conversion operator's own parameter list: the first one under the cast's
1579    declarator, past any pointer or reference. The search starts at the cast's
1580    declarator, so a parameter list inside the target type
1581    (``operator std::function<void(int)>()``) is not it."""
1582    declarator = cast.child_by_field_name("declarator")
1583    stack = [declarator] if declarator is not None else []
1584    while stack:
1585        node = stack.pop()
1586        if node.type == "parameter_list":
1587            return node
1588        stack.extend(reversed(node.children))
1589    return None
1590
1591
1592def _cpp_conversion_target_end(cast: TreeSitterNode) -> int:
1593    """Where a conversion operator's target type ends: at its own parameter list
1594    (``operator const char*() const``), past any pointer or reference."""
1595    parameters = _cpp_conversion_parameters(cast)
1596    return parameters.start_byte if parameters is not None else cast.end_byte
1597
1598
1599def _cpp_conversion(
1600    node: TreeSitterNode,
1601    cast: TreeSitterNode,
1602    containers: tuple[list[str], list[SemanticElement]],
1603    source: bytes | None,
1604    naming: _CppNaming,
1605) -> EntityName:
1606    """A conversion operator, named ``ns::W::operator const char*``: the qualifier
1607    written before the ``operator_cast``, then ``operator`` and its target type as
1608    written, up to the parameter list. Its name ends where the target type does."""
1609    prefix, outer = containers
1610    declarator = node.child_by_field_name("declarator")
1611    written = _cpp_bytes(declarator, source)
1612    head = (
1613        written[: cast.start_byte - declarator.start_byte].decode("utf-8", "replace")
1614        if declarator is not None
1615        else ""
1616    )
1617    keyword = cast.children[0] if cast.children else cast
1618    target_end = _cpp_conversion_target_end(cast)
1619    cast_text = _cpp_bytes(cast, source)
1620    target = cast_text[
1621        keyword.end_byte - cast.start_byte : target_end - cast.start_byte
1622    ].decode("utf-8", "replace")
1623    qualifier = _cpp_name(
1624        head,
1625        _cpp_template_headers(node, source),
1626        strip_last=_cpp_templated(node),
1627        lookup=naming.specs_at(node, source, cast.start_byte, head),
1628    )
1629    own = f"{qualifier}operator {_squeeze(target)}"
1630    spelled = cast_text[: target_end - cast.start_byte].decode("utf-8", "replace")
1631    # a conversion operator is always a member: its qualifier is its class
1632    own, semantic = _cpp_without_inline_parts(
1633        naming,
1634        node,
1635        source,
1636        own,
1637        cast.start_byte,
1638        [*outer, *_cpp_qualified(own, "operator", member=True)],
1639    )
1640    return _named(
1641        "operator",
1642        "::".join([*prefix, own]),
1643        semantic,
1644        cast.start_byte + len(spelled.rstrip().encode("utf-8")),
1645    )
1646
1647
1648def _cpp_describe_in(
1649    node: TreeSitterNode, source: bytes | None, naming: _CppNaming
1650) -> EntityName | None:
1651    """Name the C or C++ entity ``node``, read from ``source`` (see :func:`_cpp_text`),
1652    its containers kept as ``naming`` says."""
1653    prefix, outer = _cpp_containers(node, source, naming)
1654    if node.type in ("function_definition", "declaration", "field_declaration"):
1655        cast = _cpp_operator_cast(node)
1656        if cast is not None:
1657            return _cpp_conversion(node, cast, (prefix, outer), source, naming)
1658        if _cpp_defined_by_macro(node):
1659            return None
1660        declarator = cpp_function_declarator(node)
1661        inner = (
1662            declarator.child_by_field_name("declarator")
1663            if declarator is not None
1664            else None
1665        )
1666        if inner is None:
1667            return None
1668        written = _cpp_text_without_comments(inner, source)
1669        own = _cpp_name(
1670            written,
1671            _cpp_template_headers(node, source),
1672            strip_last=_cpp_templated(node),
1673            lookup=naming.specs_at(node, source, inner.end_byte, written),
1674        )
1675        last = own.rsplit("::", 1)[-1]
1676        qualifier = own.rsplit("::", 1)[0] if "::" in own else None
1677        if qualifier is not None:
1678            container: str | None = _strip_template_args(qualifier.rsplit("::", 1)[-1])
1679        elif _cpp_in_class(node) and prefix:
1680            container = _strip_template_args(prefix[-1])
1681        else:
1682            container = None
1683        kind: EntityKind
1684        if last.startswith("operator"):
1685            kind = "operator"
1686        elif container is not None and last == container:
1687            kind = "constructor"
1688        elif container is not None and last.startswith("~"):
1689            kind = "destructor"
1690        elif container is not None:
1691            kind = "method"
1692        else:
1693            kind = "function"
1694        own, semantic = _cpp_without_inline_parts(
1695            naming,
1696            node,
1697            source,
1698            own,
1699            inner.end_byte,
1700            [*outer, *_cpp_qualified(own, kind, member=kind in _CPP_MEMBER_KINDS)],
1701        )
1702        return _named(kind, "::".join([*prefix, own]), semantic, inner.end_byte)
1703    if node.type in _CPP_CLASS_KINDS:
1704        class_kind = _CPP_CLASS_KINDS[node.type]
1705        name = node.child_by_field_name("name")
1706        if name is None:
1707            return EntityName(class_kind, None, None)
1708        own_name = _cpp_class_name_in(name, node, source, naming)
1709        return _named(
1710            class_kind,
1711            "::".join([*prefix, own_name]),
1712            [*outer, *_cpp_qualified(own_name, class_kind, member=False)],
1713            name.end_byte,
1714        )
1715    if node.type == "namespace_definition":
1716        own_parts = _cpp_namespace_parts(node, source, own=True, naming=naming)
1717        name = node.child_by_field_name("name")
1718        return _named(
1719            "namespace",
1720            "::".join([*prefix, "::".join(own_parts)]),
1721            [*outer, *(_element("namespace", part) for part in own_parts)],
1722            name.end_byte if name is not None else None,
1723        )
1724    return None
1725
1726
1727def _cpp_describe(node: TreeSitterNode) -> EntityName | None:
1728    return _cpp_describe_in(node, None, _ALONE)
1729
1730
1731# --- C and C++: what tells a function's overloads apart -------------------------
1732#
1733# The parameter-type rules follow codebase-memory-mcp's callable signatures
1734# (``internal/cbm/callable_sig.c``, MIT): ``(void)`` is ``()``, top-level cv-qualifiers
1735# are dropped, arrays and function types decay to pointers, and ``struct X`` is ``X``.
1736# They are re-implemented here on the syntax tree; no code is taken. Leaving out
1737# comments, attributes and the parameter's name wherever they sit is ours.
1738
1739# What a parameter's type leaves out wherever it sits: a comment, a C++ ``[[...]]``
1740# attribute, a GNU ``__attribute__((...))``.
1741_CPP_PARAMETER_NOISE = frozenset(
1742    {"comment", "attribute_declaration", "attribute_specifier"}
1743)
1744
1745# The declarators that hold their inner declarator without a ``declarator`` field: it
1746# is their first named child that is no noise.
1747_CPP_UNFIELDED_DECLARATORS = frozenset(
1748    {
1749        "attributed_declarator",
1750        "reference_declarator",
1751        "abstract_reference_declarator",
1752        "parenthesized_declarator",
1753        "abstract_parenthesized_declarator",
1754        "variadic_declarator",
1755    }
1756)
1757
1758# The cv-qualifiers a parameter's top level may carry, which do not make it another
1759# type (``const int`` is ``int``, ``int* const`` is ``int*``).
1760_CPP_TOP_LEVEL_QUALIFIERS = frozenset(
1761    {"const", "volatile", "restrict", "__restrict", "__restrict__"}
1762)
1763
1764# One edit of a parameter's text: the byte range it replaces, and its replacement.
1765_Edit = tuple[int, int, str]
1766
1767
1768def _cpp_inner_declarator(node: TreeSitterNode) -> TreeSitterNode | None:
1769    """The declarator ``node`` wraps; ``None`` at the name."""
1770    if node.type in ("identifier", "field_identifier"):
1771        return None
1772    if node.type in _CPP_UNFIELDED_DECLARATORS:
1773        return next(
1774            (c for c in node.named_children if c.type not in _CPP_PARAMETER_NOISE),
1775            None,
1776        )
1777    return node.child_by_field_name("declarator")
1778
1779
1780def _cpp_declarator_chain(parameter: TreeSitterNode) -> list[TreeSitterNode]:
1781    """A parameter's declarators, from the outermost to its name (or to the innermost
1782    one of an unnamed parameter)."""
1783    chain: list[TreeSitterNode] = []
1784    current = parameter.child_by_field_name("declarator")
1785    while current is not None and len(chain) < 16:
1786        chain.append(current)
1787        current = _cpp_inner_declarator(current)
1788    return chain
1789
1790
1791def _cpp_noise_edits(
1792    node: TreeSitterNode, source: bytes | None, edits: list[_Edit]
1793) -> None:
1794    """The edits that clean a parameter's text: every comment and attribute in
1795    ``node`` dropped, and every nested parameter list (a function pointer's) replaced
1796    by its normalised form. Walked with a stack, not by recursion, so a type nested
1797    hundreds deep (``A<A<…<int>…>>``) cannot exhaust Python's recursion limit."""
1798    stack = [node]
1799    while stack:
1800        for child in stack.pop().children:
1801            if child.type in _CPP_PARAMETER_NOISE:
1802                edits.append((child.start_byte, child.end_byte, " "))
1803            elif child.type == "parameter_list":
1804                types = _cpp_parameter_types(child, source)
1805                edits.append(
1806                    (child.start_byte, child.end_byte, f"({', '.join(types)})")
1807                )
1808            else:
1809                stack.append(child)
1810
1811
1812def _cpp_apply_edits(
1813    node: TreeSitterNode, source: bytes | None, end: int, edits: list[_Edit]
1814) -> str:
1815    """The text of ``node`` up to the byte ``end``, with ``edits`` applied (an edit
1816    inside an earlier one is dropped: the earlier one rewrote that text already)."""
1817    written = _cpp_bytes(node, source)
1818    start = node.start_byte
1819    kept = sorted(
1820        (edit for edit in edits if start <= edit[0] and edit[1] <= end),
1821        # at one position an insertion comes first, then the longest replacement
1822        key=lambda edit: (edit[0], edit[0] < edit[1], -edit[1]),
1823    )
1824    out: list[str] = []
1825    position = start
1826    for edit_start, edit_end, text in kept:
1827        if edit_start < position:
1828            continue
1829        out.append(
1830            written[position - start : edit_start - start].decode("utf-8", "replace")
1831        )
1832        out.append(text)
1833        position = edit_end
1834    out.append(written[position - start : end - start].decode("utf-8", "replace"))
1835    return "".join(out)
1836
1837
1838def _cpp_decay_edits(
1839    chain: list[TreeSitterNode], holder: int | None, named: bool, edits: list[_Edit]
1840) -> str:
1841    """The edits that decay the declarator holding a parameter's name (``holder`` in
1842    ``chain``) to a pointer when it is an array or a function: ``[...]`` is ``*``, or
1843    ``(*)`` when another array applies to it from outside (``int[3][4]`` is
1844    ``int (*)[4]``); an unnamed function type gains ``(*)``. What the name itself
1845    becomes: ``(*)`` for a named function type, else nothing."""
1846    if holder is None:
1847        return ""
1848    node = chain[holder]
1849    arrays = ("array_declarator", "abstract_array_declarator")
1850    if node.type in arrays:
1851        suffix_outside = any(outer.type in arrays for outer in chain[:holder])
1852        bracket = next((c for c in node.children if c.type == "["), None)
1853        start = bracket.start_byte if bracket is not None else node.start_byte
1854        edits.append((start, node.end_byte, "(*)" if suffix_outside else "*"))
1855    elif node.type == "function_declarator" and named:
1856        return "(*)"
1857    elif node.type == "abstract_function_declarator":
1858        edits.append((node.start_byte, node.start_byte, "(*)"))
1859    return ""
1860
1861
1862def _cpp_cv_edits(
1863    parameter: TreeSitterNode,
1864    holder: TreeSitterNode | None,
1865    source: bytes | None,
1866    edits: list[_Edit],
1867) -> bool:
1868    """The edits that drop the cv-qualifiers of the parameter itself: those of the
1869    pointer that holds its name (``int* const p``, not the ``const`` of
1870    ``int* const* p``, which is its pointee's), or its own specifiers' when no
1871    declarator holds the name (``const int x``); a reference, an array or a function
1872    there has none of its own. Whether the specifiers' were the ones."""
1873    if holder is None:
1874        qualified = parameter
1875    elif holder.type in ("pointer_declarator", "abstract_pointer_declarator"):
1876        qualified = holder
1877    else:
1878        return False
1879    for child in qualified.children:
1880        if (
1881            child.type == "type_qualifier"
1882            and _cpp_text(child, source) in _CPP_TOP_LEVEL_QUALIFIERS
1883        ):
1884            edits.append((child.start_byte, child.end_byte, ""))
1885    return holder is None
1886
1887
1888def _cpp_base_type_edits(
1889    parameter: TreeSitterNode,
1890    base: TreeSitterNode,
1891    *,
1892    reorder: bool,
1893    source: bytes | None,
1894    edits: list[_Edit],
1895) -> None:
1896    """The edits on a parameter's base type ``base``: the specifiers' ``const`` and
1897    ``volatile`` (when ``reorder``: they qualify a pointee, ``int const*``) written
1898    once, in that order, before the type; an elaborated ``struct S`` as ``S``; an
1899    integer type as the compiler spells it (``unsigned`` is ``unsigned int``)."""
1900    if reorder:
1901        specifiers = list(parameter.named_children)
1902        if base.type == "sized_type_specifier":
1903            specifiers.extend(base.named_children)
1904        present = {"const": False, "volatile": False}
1905        for qualifier in specifiers:
1906            if qualifier.type != "type_qualifier":
1907                continue
1908            written = _cpp_text(qualifier, source)
1909            if written not in present:
1910                continue
1911            present[written] = True
1912            edits.append((qualifier.start_byte, qualifier.end_byte, ""))
1913        prefix = "".join(f"{word} " for word, here in present.items() if here)
1914        if prefix:
1915            edits.append((base.start_byte, base.start_byte, prefix))
1916    if base.type in _CPP_CLASS_KINDS and base.child_by_field_name("body") is None:
1917        keyword = base.children[0] if base.children else None
1918        if keyword is not None and keyword.type in ("struct", "class", "union", "enum"):
1919            edits.append((keyword.start_byte, keyword.end_byte, ""))
1920    elif base.type == "sized_type_specifier":
1921        spelled = _cpp_sized_type(base, source)
1922        if spelled is not None:
1923            edits.append((base.start_byte, base.end_byte, spelled))
1924
1925
1926def _cpp_parameter_type(parameter: TreeSitterNode, source: bytes | None) -> str:
1927    """One parameter's type, normalised on the syntax tree so that every spelling the
1928    compiler reads as one type is one string: its comments, attributes, name and
1929    default value left out; its top-level cv-qualifiers dropped (inner ones, as in
1930    ``const int*``, kept); an array or a function type decayed to a pointer
1931    (``int[3]`` is ``int*``, ``int (int)`` is ``int (*)(int)``); ``struct X`` as
1932    ``X``; a function pointer's own parameters normalised alike; whitespace
1933    collapsed, ``&`` and ``*`` against the type (``const T&``)."""
1934    default = parameter.child_by_field_name("default_value")
1935    end = default.start_byte if default is not None else parameter.end_byte
1936    edits: list[_Edit] = []
1937    _cpp_noise_edits(parameter, source, edits)
1938
1939    chain = _cpp_declarator_chain(parameter)
1940    name = (
1941        chain[-1]
1942        if chain and chain[-1].type in ("identifier", "field_identifier")
1943        else None
1944    )
1945    # the declarator that holds the name (or the innermost of an unnamed parameter)
1946    holder = next(
1947        (
1948            index
1949            for index in range(len(chain) - 1, -1, -1)
1950            if chain[index] != name
1951            and chain[index].type
1952            not in (
1953                "attributed_declarator",
1954                "parenthesized_declarator",
1955                "abstract_parenthesized_declarator",
1956            )
1957        ),
1958        None,
1959    )
1960    name_becomes = _cpp_decay_edits(chain, holder, name is not None, edits)
1961    if name is not None:
1962        edits.append((name.start_byte, name.end_byte, name_becomes))
1963    top_level = _cpp_cv_edits(
1964        parameter, chain[holder] if holder is not None else None, source, edits
1965    )
1966    base = parameter.child_by_field_name("type")
1967    if base is not None:
1968        _cpp_base_type_edits(
1969            parameter, base, reorder=not top_level, source=source, edits=edits
1970        )
1971
1972    written = _cpp_apply_edits(parameter, source, end, edits).rstrip().rstrip("=")
1973    return _cpp_spaced_pointer_groups(
1974        _collapse(written)
1975        .replace(" &", "&")
1976        .replace(" *", "*")
1977        .replace("( ", "(")
1978        .replace(" )", ")")
1979    )
1980
1981
1982def _cpp_sized_type(node: TreeSitterNode, source: bytes | None) -> str | None:
1983    """A ``sized_type_specifier`` (``unsigned``, ``long int``, ``signed char``) spelled
1984    as the compiler spells its type: ``unsigned int``, ``long``, ``signed char``,
1985    ``long double``; ``signed`` and a redundant ``int`` dropped, ``char``'s sign kept
1986    (``char``, ``signed char`` and ``unsigned char`` are three types). ``None`` for a
1987    shape this does not know, which is then compared as written."""
1988    unsigned = short = signed = False
1989    longs = 0
1990    base: str | None = None
1991    for child in node.children:
1992        if child.type == "unsigned":
1993            unsigned = True
1994        elif child.type == "signed":
1995            signed = True
1996        elif child.type == "short":
1997            short = True
1998        elif child.type == "long":
1999            longs += 1
2000        elif child.type == "primitive_type":
2001            base = _cpp_text(child, source)
2002        elif child.type not in ("type_qualifier", "comment"):
2003            return None
2004    sign = "unsigned " if unsigned else ""
2005    if base == "char" and not short and longs == 0:
2006        return "unsigned char" if unsigned else "signed char" if signed else "char"
2007    if base == "double" and not short and longs == 1 and not unsigned and not signed:
2008        return "long double"
2009    if base not in (None, "int"):
2010        return None
2011    if short and longs == 0:
2012        return f"{sign}short"
2013    if not short and longs in (0, 1, 2):
2014        return f"{sign}{('int', 'long', 'long long')[longs]}"
2015    return None
2016
2017
2018def _cpp_spaced_pointer_groups(written: str) -> str:
2019    """``written`` with one space before each parenthesised pointer ``(*`` that follows
2020    a word, as ``int (*)(int)`` is spelled however it was written."""
2021    out: list[str] = []
2022    previous: str | None = None
2023    for index, char in enumerate(written):
2024        if (
2025            char == "("
2026            and written.startswith("*", index + 1)
2027            and previous is not None
2028            and previous not in " ("
2029        ):
2030            out.append(" ")
2031        out.append(char)
2032        previous = char
2033    return "".join(out)
2034
2035
2036def _cpp_parameter_types(parameters: TreeSitterNode, source: bytes | None) -> list[str]:
2037    """The normalised types of a parameter list's parameters (``...`` for C varargs);
2038    none for ``(void)``."""
2039    types: list[str] = []
2040    for parameter in parameters.children:
2041        if parameter.type in (
2042            "parameter_declaration",
2043            "optional_parameter_declaration",
2044            "variadic_parameter_declaration",
2045        ):
2046            types.append(_cpp_parameter_type(parameter, source))
2047        elif parameter.type in ("variadic_parameter", "..."):
2048            types.append("...")
2049    return [] if types == ["void"] else types
2050
2051
2052def cpp_signature(node: TreeSitterNode, source: bytes | None) -> str | None:
2053    """What tells a function's overloads apart: its normalised parameter types,
2054    ``, ``-joined in parentheses, then the cv- and ref-qualifiers of a member function
2055    as written (``(int) const``, ``() &&``). ``noexcept``, ``override`` and a trailing
2056    return type are left out: no overload differs by them alone."""
2057    cast = _cpp_operator_cast(node)
2058    if cast is not None:
2059        # a conversion operator's parameters and qualifiers are its cast's
2060        # (``operator bool() const``), not those of a list inside the target type
2061        parameters = _cpp_conversion_parameters(cast)
2062        declarator = parameters.parent if parameters is not None else None
2063    else:
2064        declarator = cpp_function_declarator(node)
2065        parameters = (
2066            declarator.child_by_field_name("parameters")
2067            if declarator is not None
2068            else None
2069        )
2070    if declarator is None or parameters is None:
2071        return None
2072    signature = f"({', '.join(_cpp_parameter_types(parameters, source))})"
2073    for child in declarator.children:
2074        if child.type == "type_qualifier":
2075            qualifier = _cpp_text(child, source)
2076            if qualifier not in ("const", "volatile"):
2077                continue
2078        elif child.type == "ref_qualifier":
2079            qualifier = _cpp_text(child, source)
2080        else:
2081            continue
2082        signature += f" {qualifier}"
2083    return signature
2084
2085
2086# --- C and C++: the names a whole file decides ---------------------------------
2087
2088
2089def _cpp_qualified_with_arguments(node: TreeSitterNode, source: bytes | None) -> bool:
2090    """``node`` may be named otherwise by the file's partial specialisations
2091    (:class:`_PartialSpecs`): a function whose name is qualified with a template
2092    argument list."""
2093    if node.type not in ("function_definition", "declaration", "field_declaration"):
2094        return False
2095    cast = _cpp_operator_cast(node)
2096    if cast is not None:
2097        declarator = node.child_by_field_name("declarator")
2098        if declarator is None:
2099            return False
2100        head = _cpp_bytes(declarator, source)[: cast.start_byte - declarator.start_byte]
2101        return b"<" in head
2102    declarator = cpp_function_declarator(node)
2103    inner = declarator.child_by_field_name("declarator") if declarator else None
2104    return (
2105        inner is not None
2106        and inner.type == "qualified_identifier"
2107        and b"<" in _cpp_bytes(inner, source)
2108    )
2109
2110
2111def _cpp_has_body_entity(node: TreeSitterNode) -> bool:
2112    """``node`` is a definition with a body, which a name must tell apart from every
2113    other: a function, a class, struct, union or enum with its body, a namespace."""
2114    if node.type in ("function_definition", "namespace_definition"):
2115        return True
2116    return node.type in _CPP_CLASS_KINDS and _cpp_has_body(node)
2117
2118
2119def _cpp_leaf_key(name: str) -> str:
2120    """What two names that could be the same end in: the last part of ``name`` without
2121    its argument list, every conversion operator as ``operator `` (it only picks which
2122    entities :func:`cpp_renames` compares)."""
2123    last = _cpp_split(name)[-1]
2124    if _cpp_operator_at(last, 0):
2125        return "operator " if last[len("operator") :].startswith(" ") else last
2126    return _strip_template_args(last)
2127
2128
2129def _cpp_written_leaf(node: TreeSitterNode, source: bytes | None) -> str | None:
2130    """:func:`_cpp_leaf_key` of ``node``'s name, read from its name node without naming
2131    it."""
2132    if _cpp_operator_cast(node) is not None:
2133        return "operator "
2134    if node.type in ("function_definition", "declaration", "field_declaration"):
2135        declarator = cpp_function_declarator(node)
2136        name = (
2137            declarator.child_by_field_name("declarator")
2138            if declarator is not None
2139            else None
2140        )
2141    elif node.type == "namespace_definition":
2142        name = node.child_by_field_name("name")
2143        if name is None:
2144            return "(anonymous namespace)"
2145    else:
2146        name = node.child_by_field_name("name")
2147    if name is None:
2148        return None
2149    return _cpp_leaf_key(_squeeze(_cpp_text_without_comments(name, source)))
2150
2151
2152@dataclass(frozen=True)
2153class _Seen:
2154    """One entity :func:`cpp_renames` compares."""
2155
2156    node: TreeSitterNode
2157    alone: EntityName
2158    """Its name as the node alone gives it."""
2159    named: EntityName
2160    """Its name with what the file adds to one node."""
2161    full: EntityName | None
2162    """Its name with every container kept, when one is left out."""
2163
2164
2165@dataclass
2166class _Found:
2167    """What the one walk of :func:`cpp_renames` finds."""
2168
2169    spec_nodes: list[TreeSitterNode] = field(default_factory=list)
2170    """The classes that may be partial specialisations (:func:`_cpp_templated_class`),
2171    read once the inline namespaces are known."""
2172    inline_paths: set[str] = field(default_factory=set)
2173    """The paths of its inline namespaces (:func:`_cpp_inline_paths_of`)."""
2174    inline_names: set[str] = field(default_factory=set)
2175    """Their own names (``v2``), one of which a qualifier through one holds."""
2176    left_out: list[TreeSitterNode] = field(default_factory=list)
2177    """The entities that leave a container out."""
2178    bodies: list[TreeSitterNode] = field(default_factory=list)
2179    """The other definitions."""
2180    qualified: list[TreeSitterNode] = field(default_factory=list)
2181    """The other functions qualified with arguments."""
2182    through: list[TreeSitterNode] = field(default_factory=list)
2183    """The functions named through a written qualifier
2184    (:func:`_cpp_written_qualifier`)."""
2185
2186
2187def _cpp_written_qualifier(node: TreeSitterNode, source: bytes | None) -> str | None:
2188    """The written qualified name of the function ``node`` (``lib::v2::g`` of
2189    ``void lib::v2::g() {}``, ``lib::v1::W::`` of a conversion operator
2190    ``lib::v1::W::operator int()``), when it has one."""
2191    if node.type not in ("function_definition", "declaration", "field_declaration"):
2192        return None
2193    cast = _cpp_operator_cast(node)
2194    if cast is not None:
2195        declarator = node.child_by_field_name("declarator")
2196        if declarator is None:
2197            return None
2198        head = _cpp_bytes(declarator, source)[: cast.start_byte - declarator.start_byte]
2199        return head.decode("utf-8", "replace") if b"::" in head else None
2200    declarator = cpp_function_declarator(node)
2201    inner = declarator.child_by_field_name("declarator") if declarator else None
2202    if inner is None or inner.type != "qualified_identifier":
2203        return None
2204    return _cpp_text(inner, source)
2205
2206
2207def _cpp_class_through_inline(
2208    node: TreeSitterNode, source: bytes | None, inline_names: set[str]
2209) -> bool:
2210    """The class ``node``'s head is written through a qualifier that holds the name of
2211    an inline namespace of the file met so far (``struct lib::v2::X {…}``, one of
2212    ``inline_names``)."""
2213    if node.type not in _CPP_CLASS_KINDS:
2214        return False
2215    name = node.child_by_field_name("name")
2216    # tree-sitter-cpp reads a qualified class name as this kind
2217    return (
2218        name is not None
2219        and name.type == "qualified_identifier"
2220        and any(word in inline_names for word in _cpp_words(_cpp_text(name, source)))
2221    )
2222
2223
2224def cpp_renames(
2225    root: TreeSitterNode, source: bytes | None, prefix: Callable[[int], str]
2226) -> dict[int, EntityName]:
2227    """The C++ entities under ``root`` named otherwise than the node alone names them,
2228    by the end of their name, with that name.
2229
2230    An out-of-line qualifier the same as a partial specialisation of the file but for
2231    its parameter names is spelled as that specialisation (:class:`_PartialSpecs`).
2232
2233    An inline namespace and a friend's classes are left out of a name
2234    (:func:`_cpp_containers`), and so is an inline namespace of the file a written
2235    qualifier names (``void lib::v2::g() {}`` is ``lib::g``, its full chain
2236    ``lib::v2::g``: :func:`_cpp_without_inline_parts`), except where that makes two
2237    definitions with a body share a name and parameters that the full chain kept
2238    apart: then every entity of that name and parameters that leaves a container out
2239    (its declarations too) keeps the full chain, so no marker credits another
2240    definition, and so does everything a class or namespace that keeps it holds. A
2241    declaration and one definition do not collide (a friend declared in its class and
2242    defined in its namespace stays one entity). The name compared is ``prefix`` (the
2243    namespaces the file's own macros open around a name ending at a byte) and the
2244    name, and the parameters are the normalised signature (:func:`cpp_signature`).
2245
2246    Only the entities that leave a container out, and the definitions whose name ends
2247    as one of theirs does, are named here: a file with neither friends nor inline
2248    namespaces costs one walk of its tree, which skips every function body.
2249    """
2250    # one walk: the partial specialisations, the inline namespaces, what leaves a
2251    # container out, the other definitions, the functions a specialisation may spell,
2252    # and those named through a written qualifier
2253    found = _Found()
2254    cursor = root.walk()
2255    # whether an inline namespace is around the cursor's node, per depth
2256    inside = [False]
2257    while True:
2258        node = cursor.node
2259        if node is None:  # a cursor always stands on a node
2260            return _cpp_renamed(source, prefix, found)
2261        in_inline = inside[-1]
2262        inline_here = node.type == "namespace_definition" and (
2263            _cpp_inline_namespace_or_part(node)
2264            or _cpp_reopens_inline(node, source, prefix, found.inline_paths)
2265        )
2266        # a class head written through an inline namespace leaves it out, and so does
2267        # everything the class holds
2268        through_here = not inline_here and _cpp_class_through_inline(
2269            node, source, found.inline_names
2270        )
2271        if _cpp_templated_class(node):
2272            found.spec_nodes.append(node)
2273        if inline_here:
2274            for path in _cpp_inline_paths_of(node, source, prefix):
2275                found.inline_names.add(path.rsplit("::", 1)[-1])
2276                found.inline_paths.add(path)
2277        if is_entity(node, CommentType.cpp):
2278            if _cpp_written_qualifier(node, source) is not None:
2279                found.through.append(node)
2280            if in_inline or inline_here or through_here or _cpp_is_friend(node):
2281                found.left_out.append(node)
2282            else:
2283                if _cpp_has_body_entity(node):
2284                    found.bodies.append(node)
2285                if _cpp_qualified_with_arguments(node, source):
2286                    found.qualified.append(node)
2287        # nothing a function's body holds is a friend, an inline namespace or a
2288        # partial specialisation, or names one
2289        if node.type != "function_definition" and cursor.goto_first_child():
2290            inside.append(in_inline or inline_here or through_here)
2291            continue
2292        while not cursor.goto_next_sibling():
2293            if not cursor.goto_parent():
2294                return _cpp_renamed(source, prefix, found)
2295            inside.pop()
2296
2297
2298def _cpp_leave_out_through_inline(
2299    source: bytes | None, naming: _CppNaming, found: _Found
2300) -> set[int]:
2301    """The functions of ``found.through`` whose written qualifier may name an inline
2302    namespace of the file (one of its inline names is among its words) that ``naming``
2303    names otherwise than as written and that are not in ``found.left_out``, moved there
2304    from ``found.bodies``, by node id: they leave a container out, their full chain the
2305    qualifier as written."""
2306    if naming.inline is None:
2307        return set()
2308    through = [
2309        node
2310        for node in found.through
2311        if (head := _cpp_written_qualifier(node, source)) is not None
2312        and any(word in found.inline_names for word in _cpp_words(head))
2313    ]
2314    left = {node.id for node in found.left_out}
2315    as_written = replace(naming, inline=None)
2316    moved: set[int] = set()
2317    for node in through:
2318        if node.id in left:
2319            continue
2320        named = _cpp_describe_in(node, source, naming)
2321        written = _cpp_describe_in(node, source, as_written)
2322        if (named and named.qualified_name) != (written and written.qualified_name):
2323            moved.add(node.id)
2324            found.left_out.append(node)
2325    found.bodies = [node for node in found.bodies if node.id not in moved]
2326    return moved
2327
2328
2329def _cpp_renamed(
2330    source: bytes | None, prefix: Callable[[int], str], found: _Found
2331) -> dict[int, EntityName]:
2332    """:func:`cpp_renames`, once its walk has found what it looks for
2333    (:class:`_Found`)."""
2334    base = _CppNaming(
2335        inline=frozenset(found.inline_paths) if found.inline_paths else None,
2336        macros=prefix,
2337    )
2338    specs = _PartialSpecs.of(found.spec_nodes, source, base)
2339    with_specs = replace(base, specs=specs if specs.by_template else None)
2340    full_chain = replace(with_specs, full_chain=True)
2341    moved = _cpp_leave_out_through_inline(source, with_specs, found)
2342
2343    def named_alone(node: TreeSitterNode) -> tuple[EntityName, EntityName] | None:
2344        alone = _cpp_describe_in(node, source, _ALONE)
2345        if alone is None or alone.qualified_name is None or alone.name_end is None:
2346            return None
2347        named = alone
2348        if with_specs.inline is not None or (
2349            with_specs.specs is not None and _cpp_qualified_with_arguments(node, source)
2350        ):
2351            named = _cpp_describe_in(node, source, with_specs) or alone
2352        return alone, named
2353
2354    out: dict[int, EntityName] = {}
2355    for node in found.qualified if with_specs.specs is not None else ():
2356        if node.id in moved:
2357            continue
2358        names = named_alone(node)
2359        if names is None:
2360            continue
2361        alone, named = names
2362        if named.qualified_name != alone.qualified_name and named.name_end is not None:
2363            out[named.name_end] = named
2364    # the entities that leave a container out, named both ways
2365    seen: list[_Seen] = []
2366    for node in found.left_out:
2367        names = named_alone(node)
2368        if names is None:
2369            continue
2370        alone, named = names
2371        full = _cpp_describe_in(node, source, full_chain)
2372        if full is not None and full.qualified_name == named.qualified_name:
2373            full = None
2374        if (
2375            full is None
2376            and named.qualified_name != alone.qualified_name
2377            and named.name_end is not None
2378        ):
2379            out[named.name_end] = named
2380        seen.append(_Seen(node, alone, named, full))
2381    leaves = {
2382        _cpp_leaf_key(entity.named.qualified_name)
2383        for entity in seen
2384        if entity.full is not None and entity.named.qualified_name is not None
2385    }
2386    if not leaves:
2387        return out
2388    # the other definitions that could share a name with one of them
2389    for node in found.bodies:
2390        if _cpp_written_leaf(node, source) in leaves:
2391            names = named_alone(node)
2392            if names is not None:
2393                seen.append(_Seen(node, *names, None))
2394    _cpp_keep_full_chains_where_names_collide(seen, source, prefix, out)
2395    return out
2396
2397
2398def _cpp_chain_of(name: EntityName, prefix: Callable[[int], str]) -> str:
2399    """``name`` after the namespaces the file's macros open around it: what two full
2400    chains are compared by."""
2401    at = prefix(name.name_end) if name.name_end is not None else ""
2402    return at + (name.qualified_name or "")
2403
2404
2405def _cpp_held_by(chain: str, containers: set[str]) -> bool:
2406    """The chain ``chain`` is inside one of ``containers`` (one of them is a leading
2407    part of it, ``lib::v1::Widget`` of ``lib::v1::Widget::draw``)."""
2408    parts = _cpp_split(chain)
2409    return any(
2410        "::".join(parts[: count + 1]) in containers for count in range(len(parts) - 1)
2411    )
2412
2413
2414def _cpp_keep_full_chains_where_names_collide(
2415    seen: list[_Seen],
2416    source: bytes | None,
2417    prefix: Callable[[int], str],
2418    out: dict[int, EntityName],
2419) -> None:
2420    """The names of ``seen`` into ``out`` where they differ from the names alone: each
2421    one that leaves a container out keeps its full chain where its name and
2422    parameters are those of two definitions whose full chains differ, and so does
2423    everything a class or namespace that keeps its full chain holds (its members are
2424    named in it), but a friend, which is named in its namespace."""
2425    keys = [
2426        (_cpp_chain_of(entity.named, prefix), cpp_signature(entity.node, source))
2427        for entity in seen
2428    ]
2429    chains: dict[tuple[str, str | None], set[str]] = {}
2430    for entity, key in zip(seen, keys, strict=True):
2431        if _cpp_has_body_entity(entity.node):
2432            full = entity.full or entity.named
2433            chains.setdefault(key, set()).add(_cpp_chain_of(full, prefix))
2434    collides = [
2435        entity.full is not None and len(chains.get(key, ())) > 1
2436        for entity, key in zip(seen, keys, strict=True)
2437    ]
2438    # the classes and namespaces that keep their full chain
2439    kept = {
2440        _cpp_chain_of(entity.full, prefix)
2441        for entity, collide in zip(seen, collides, strict=True)
2442        if collide
2443        and entity.full is not None
2444        and _cpp_has_body_entity(entity.node)
2445        and entity.node.type != "function_definition"
2446    }
2447    for entity, collide in zip(seen, collides, strict=True):
2448        if entity.full is None:
2449            continue
2450        # a friend is named in its namespace, never in its class
2451        keeps = collide or (
2452            bool(kept)
2453            and not _cpp_is_friend(entity.node)
2454            and _cpp_held_by(_cpp_chain_of(entity.full, prefix), kept)
2455        )
2456        name = entity.full if keeps else entity.named
2457        if (
2458            name.qualified_name != entity.alone.qualified_name
2459            and name.name_end is not None
2460        ):
2461            out[name.name_end] = name
2462
2463
2464class CppNames:
2465    """What the names of a C++ file's entities take from the whole file, beyond what
2466    one node shows: the namespaces the file's own macros open
2467    (:class:`~sphinx_codelinks.analyse.cpp_macros.MacroScopes`), and the entities named
2468    otherwise than one node names them (:func:`cpp_renames`: a name spelled as the
2469    file's partial specialisation spells it; an inline namespace or a friend's classes
2470    kept where leaving them out would give two definitions one name).
2471
2472    Made once per parse, by :func:`sphinx_codelinks.analyse.utils.parse_source`; the
2473    renames are worked out when a marker of the file first asks for a name, so a file
2474    without markers costs nothing.
2475    """
2476
2477    def __init__(
2478        self, source: bytes, root: TreeSitterNode, macros: MacroScopes | None = None
2479    ) -> None:
2480        self.source = source
2481        """The file as written: what every name is read from."""
2482        self.macros = macros if macros is not None else MacroScopes()
2483        """The namespaces the file's own macros open."""
2484        self._root = root
2485        self._renamed: dict[int, EntityName] | None = None
2486
2487    @property
2488    def renamed(self) -> dict[int, EntityName]:
2489        """The entities named otherwise, by the end of their name: the name and its
2490        parts, before the macros' namespaces."""
2491        if self._renamed is None:
2492            self._renamed = cpp_renames(self._root, self.source, self._prefix)
2493        return self._renamed
2494
2495    def _prefix(self, name_end: int) -> str:
2496        """The namespaces the file's macros open around a name ending at ``name_end``,
2497        each followed by ``::``."""
2498        return "".join(f"{n}::" for n in self.macros.namespaces_at(name_end))
2499
2500    def qualify(self, name: EntityName, start_line: int) -> EntityName:
2501        """``name``, an entity's name as its node alone gives it, as the whole file
2502        names it: renamed where the file decides so, then prefixed with the namespaces
2503        the file's macros open around the name's end, or, for an entity with no name
2504        node (an anonymous namespace), around ``start_line``, its 1-based first
2505        line."""
2506        qualified_name, semantic = name.qualified_name, name.semantic
2507        if name.name_end is not None:
2508            renamed = self.renamed.get(name.name_end)
2509            if renamed is not None:
2510                qualified_name, semantic = renamed.qualified_name, renamed.semantic
2511            namespaces = self.macros.namespaces_at(name.name_end)
2512        else:
2513            namespaces = self.macros.namespaces_at_line(start_line)
2514        if namespaces:
2515            if qualified_name is not None:
2516                qualified_name = "::".join([*namespaces, qualified_name])
2517            if semantic is not None:
2518                semantic = (
2519                    *(_element("namespace", namespace) for namespace in namespaces),
2520                    *semantic,
2521                )
2522        return replace(name, qualified_name=qualified_name, semantic=semantic)
2523
2524
2525# --- Python ------------------------------------------------------------------
2526
2527
2528def _python_semantic(definitions: list[tuple[str, str]]) -> list[SemanticElement]:
2529    """The components of a ``__qualname__``, from its definitions (node type, name),
2530    outermost first: a class, or a function -- a ``method`` directly in a class.
2531    CPython's ``<locals>`` is not one: :func:`render_semantic` writes it."""
2532    elements: list[SemanticElement] = []
2533    container: str | None = None
2534    for node_type, name in definitions:
2535        if node_type == "class_definition":
2536            kind = "class"
2537        else:
2538            kind = "method" if container == "class_definition" else "function"
2539        elements.append(_element(kind, name))
2540        container = node_type
2541    return elements
2542
2543
2544def _python_describe(node: TreeSitterNode) -> EntityName | None:
2545    """CPython's ``__qualname__``: ``Class.method``, ``outer.<locals>.inner``."""
2546    if node.type not in ("function_definition", "class_definition"):
2547        return None
2548    name = node.child_by_field_name("name")
2549    if name is None:
2550        return None
2551    segments: list[str] = []
2552    definitions = [(node.type, _text(name))]
2553    nearest: str | None = None
2554    parent = node.parent
2555    while parent is not None:
2556        if parent.type in ("class_definition", "function_definition"):
2557            nearest = nearest or parent.type
2558            parent_name = _text(parent.child_by_field_name("name"))
2559            segments.append(
2560                parent_name
2561                if parent.type == "class_definition"
2562                else f"{parent_name}.<locals>"
2563            )
2564            definitions.append((parent.type, parent_name))
2565        parent = parent.parent
2566    qualified = ".".join([*reversed(segments), _text(name)])
2567    semantic = _python_semantic(definitions[::-1])
2568    if node.type == "class_definition":
2569        return _named("class", qualified, semantic)
2570    return _named(
2571        "method" if nearest == "class_definition" else "function", qualified, semantic
2572    )
2573
2574
2575# --- Rust --------------------------------------------------------------------
2576
2577_RUST_KINDS: dict[str, EntityKind] = {
2578    "function_item": "function",
2579    "function_signature_item": "function",
2580    "struct_item": "struct",
2581    "enum_item": "enum",
2582    "union_item": "union",
2583    "trait_item": "trait",
2584    "mod_item": "module",
2585    "type_item": "type",
2586    "macro_definition": "macro",
2587}
2588
2589
2590def _rust_modules(node: TreeSitterNode) -> list[str]:
2591    segments: list[str] = []
2592    parent = node.parent
2593    while parent is not None:
2594        if parent.type == "mod_item":
2595            segments.append(_text(parent.child_by_field_name("name")))
2596        parent = parent.parent
2597    return list(reversed(segments))
2598
2599
2600def _rust_type(node: TreeSitterNode | None, modules: list[str]) -> str:
2601    """A type or trait as written: whitespace collapsed, a leading ``crate::`` dropped,
2602    and a bare name prefixed with the in-file module path."""
2603    text = _collapse(_text(node)).removeprefix("crate::")
2604    bare = node is not None and node.type in ("type_identifier", "generic_type")
2605    if bare and "::" not in text and modules:
2606        return "::".join([*modules, text])
2607    return text
2608
2609
2610# The self types rustc prints without brackets in an inherent impl's path: a path
2611# (``Widget``, ``a::Widget``, ``Widget<T>``) or a primitive.
2612_RUST_PATH_TYPES = frozenset(
2613    {"type_identifier", "generic_type", "scoped_type_identifier", "primitive_type"}
2614)
2615
2616
2617def _rust_impl_segment(impl: TreeSitterNode) -> str:
2618    """``Widget`` for an inherent impl, ``<dyn Shape>`` for an inherent impl of a type
2619    that is no path, ``<Widget as Shape>`` for a trait impl."""
2620    modules = _rust_modules(impl)
2621    self_node = impl.child_by_field_name("type")
2622    self_type = _rust_type(self_node, modules)
2623    trait = impl.child_by_field_name("trait")
2624    if trait is None:
2625        if self_node is not None and self_node.type not in _RUST_PATH_TYPES:
2626            return f"<{self_type}>"
2627        return self_type
2628    return f"<{self_type} as {_rust_type(trait, modules)}>"
2629
2630
2631def _rust_impl_parts(impl: TreeSitterNode) -> list[SemanticElement]:
2632    """An impl as components: the in-file modules it is written in, each a ``module``
2633    component of its own, then the impl, named after its self type as written and,
2634    for a trait impl, carrying the trait as the qualified name spells it.
2635
2636    The self type is written without the modules (``G`` in ``mod m``): rendering
2637    prefixes them again where the qualified name does (:func:`_rust_render`). A path
2638    from the crate root keeps its ``crate::`` where dropping it would leave a name the
2639    rendering prefixes (``crate::G`` in ``mod k`` is ``Q``, not ``k::Q``)."""
2640    modules = _rust_modules(impl)
2641    self_node = impl.child_by_field_name("type")
2642    written = _collapse(_text(self_node))
2643    name = written.removeprefix("crate::")
2644    prefixed = (
2645        bool(modules)
2646        and self_node is not None
2647        and self_node.type in ("type_identifier", "generic_type")
2648        and "::" not in name
2649    )
2650    if not prefixed and modules and written != name and _rust_resolves_in_module(name):
2651        name = written
2652    element = _element("impl", name)
2653    trait = impl.child_by_field_name("trait")
2654    if trait is not None:
2655        element["trait"] = _rust_type(trait, modules)
2656    return [*(_element("module", module) for module in modules), element]
2657
2658
2659def _rust_functions(functions: list[str], *, in_item: bool) -> list[SemanticElement]:
2660    """The functions between an item and its impl, trait or module, outermost first:
2661    the one directly in an impl or a trait (``in_item``) is a method."""
2662    return [
2663        _element("method" if in_item and index == 0 else "function", name)
2664        for index, name in enumerate(functions)
2665    ]
2666
2667
2668def _rust_describe(node: TreeSitterNode) -> EntityName | None:
2669    if node.type == "impl_item":
2670        return _named("impl", _rust_impl_segment(node), _rust_impl_parts(node))
2671    kind = _RUST_KINDS.get(node.type)
2672    name = node.child_by_field_name("name")
2673    if kind is None or name is None:
2674        return None
2675    own = _text(name)
2676    is_fn = node.type in ("function_item", "function_signature_item")
2677    functions: list[str] = []
2678    parent = node.parent
2679    while parent is not None:
2680        # a function directly in an impl or a trait is a method; one nested in a
2681        # method is a function
2682        method = is_fn and not functions
2683        own_kind: EntityKind = "method" if method else kind
2684        if parent.type == "impl_item":
2685            outer = list(reversed(functions))
2686            return _named(
2687                own_kind,
2688                "::".join([_rust_impl_segment(parent), *outer, own]),
2689                [
2690                    *_rust_impl_parts(parent),
2691                    *_rust_functions(outer, in_item=True),
2692                    _element(own_kind, own),
2693                ],
2694            )
2695        if parent.type == "trait_item":
2696            modules = _rust_modules(parent)
2697            trait_name = _text(parent.child_by_field_name("name"))
2698            outer = list(reversed(functions))
2699            return _named(
2700                own_kind,
2701                "::".join([*modules, trait_name, *outer, own]),
2702                [
2703                    *(_element("module", module) for module in modules),
2704                    _element("trait", trait_name),
2705                    *_rust_functions(outer, in_item=True),
2706                    _element(own_kind, own),
2707                ],
2708            )
2709        if parent.type == "function_item":
2710            functions.append(_text(parent.child_by_field_name("name")))
2711        if parent.type == "mod_item":
2712            break
2713        parent = parent.parent
2714    modules = _rust_modules(node)
2715    outer = list(reversed(functions))
2716    return _named(
2717        kind,
2718        "::".join([*modules, *outer, own]),
2719        [
2720            *(_element("module", module) for module in modules),
2721            *_rust_functions(outer, in_item=False),
2722            _element(kind, own),
2723        ],
2724    )
2725
2726
2727# --- C# ----------------------------------------------------------------------
2728
2729_CSHARP_KINDS: dict[str, EntityKind] = {
2730    "class_declaration": "class",
2731    "struct_declaration": "struct",
2732    "interface_declaration": "interface",
2733    "record_declaration": "record",
2734    "enum_declaration": "enum",
2735    "namespace_declaration": "namespace",
2736    "file_scoped_namespace_declaration": "namespace",
2737    "method_declaration": "method",
2738    "constructor_declaration": "constructor",
2739    "destructor_declaration": "destructor",
2740    "property_declaration": "property",
2741    "indexer_declaration": "indexer",
2742    "operator_declaration": "operator",
2743    "conversion_operator_declaration": "operator",
2744    "event_declaration": "event",
2745    "delegate_declaration": "delegate",
2746    "local_function_statement": "function",
2747}
2748_CSHARP_CONTAINERS = {
2749    "class_declaration",
2750    "struct_declaration",
2751    "interface_declaration",
2752    "record_declaration",
2753    "enum_declaration",
2754    "namespace_declaration",
2755    "method_declaration",
2756    "constructor_declaration",
2757    "local_function_statement",
2758}
2759
2760
2761def _csharp_own_name(node: TreeSitterNode) -> str | None:
2762    if node.type == "indexer_declaration":
2763        return "this[]"
2764    if node.type == "operator_declaration":
2765        symbol = next(
2766            (
2767                node.children[index + 1]
2768                for index, child in enumerate(node.children[:-1])
2769                if child.type == "operator"
2770            ),
2771            None,
2772        )
2773        return f"operator {_text(symbol)}" if symbol is not None else None
2774    if node.type == "conversion_operator_declaration":
2775        return f"operator {_squeeze(_text(node.child_by_field_name('type')))}"
2776    name = node.child_by_field_name("name")
2777    if name is None:
2778        return None
2779    own = _squeeze(_text(name))
2780    return f"~{own}" if node.type == "destructor_declaration" else own
2781
2782
2783def _csharp_elements(node: TreeSitterNode, own: str) -> list[SemanticElement]:
2784    """``node``, named ``own``, as components: a namespace's dotted name
2785    (``Acme.Gfx``) is a namespace per part."""
2786    kind = _CSHARP_KINDS[node.type]
2787    if kind == "namespace":
2788        return [_element(kind, part) for part in own.split(".")]
2789    return [_element(kind, own)]
2790
2791
2792def _csharp_describe(node: TreeSitterNode) -> EntityName | None:
2793    kind = _CSHARP_KINDS.get(node.type)
2794    if kind is None:
2795        return None
2796    segments: list[str] = []
2797    groups: list[list[SemanticElement]] = []
2798    parent = node.parent
2799    root = node
2800    while parent is not None:
2801        if parent.type in _CSHARP_CONTAINERS:
2802            container = _csharp_own_name(parent)
2803            if container is not None:
2804                segments.append(container)
2805                groups.append(_csharp_elements(parent, container))
2806        root = parent
2807        parent = parent.parent
2808    segments.reverse()
2809    groups.reverse()
2810    # a file-scoped namespace (``namespace A.B;``) is a sibling of what it holds
2811    if node.type != "file_scoped_namespace_declaration":
2812        for child in root.named_children:
2813            if child.start_byte >= node.start_byte:
2814                break
2815            if child.type == "file_scoped_namespace_declaration":
2816                namespace = _csharp_own_name(child) or ""
2817                segments.insert(0, namespace)
2818                groups.insert(0, _csharp_elements(child, namespace))
2819    own = _csharp_own_name(node)
2820    return _named(
2821        kind,
2822        _join(".", segments, own),
2823        [
2824            *(element for group in groups for element in group),
2825            *_csharp_elements(node, own or ""),
2826        ],
2827    )
2828
2829
2830# --- Go ----------------------------------------------------------------------
2831
2832
2833def _go_spec_kind(spec: TreeSitterNode) -> EntityKind:
2834    value = spec.child_by_field_name("type")
2835    if spec.type == "type_spec" and value is not None:
2836        if value.type == "struct_type":
2837            return "struct"
2838        if value.type == "interface_type":
2839            return "interface"
2840    return "type"
2841
2842
2843def _go_receiver(node: TreeSitterNode) -> str | None:
2844    """The receiver's type name, as ``go doc`` writes it (no ``*``, no type arguments)."""
2845    receiver = node.child_by_field_name("receiver")
2846    if receiver is None:
2847        return None
2848    for parameter in receiver.named_children:
2849        value = parameter.child_by_field_name("type")
2850        while value is not None and value.type in ("pointer_type", "generic_type"):
2851            value = (
2852                value.named_children[0]
2853                if value.type == "pointer_type" and value.named_children
2854                else value.child_by_field_name("type")
2855            )
2856        if value is not None:
2857            return _text(value)
2858    return None
2859
2860
2861def _go_describe(node: TreeSitterNode) -> EntityName | None:
2862    if node.type == "function_declaration":
2863        own = _text(node.child_by_field_name("name")) or None
2864        return _named("function", own, [_element("function", own or "")])
2865    if node.type == "method_declaration":
2866        own = _text(node.child_by_field_name("name"))
2867        # the receiver's type: what it is, its declaration (maybe in another file) says
2868        receiver = _go_receiver(node)
2869        if receiver:
2870            return _named(
2871                "method",
2872                f"{receiver}.{own}",
2873                [_element("type", receiver), _element("method", own)],
2874            )
2875        return _named("method", own, [_element("method", own)])
2876    if node.type == "type_declaration":
2877        specs = [
2878            child
2879            for child in node.named_children
2880            if child.type in ("type_spec", "type_alias")
2881        ]
2882        if len(specs) != 1:
2883            return EntityName("type", None, None)
2884        node = specs[0]
2885    if node.type in ("type_spec", "type_alias"):
2886        spec_kind = _go_spec_kind(node)
2887        spec_name = _text(node.child_by_field_name("name")) or None
2888        return _named(spec_kind, spec_name, [_element(spec_kind, spec_name or "")])
2889    if node.type == "method_elem":
2890        own = _text(node.child_by_field_name("name"))
2891        parent = node.parent
2892        while parent is not None and parent.type != "type_spec":
2893            parent = parent.parent
2894        owner = _text(parent.child_by_field_name("name")) if parent is not None else ""
2895        if parent is not None and owner:
2896            return _named(
2897                "method",
2898                f"{owner}.{own}",
2899                [_element(_go_spec_kind(parent), owner), _element("method", own)],
2900            )
2901        return _named("method", own, [_element("method", own)])
2902    return None
2903
2904
2905# --- TypeScript and JavaScript ----------------------------------------------
2906
2907_TS_KINDS: dict[str, EntityKind] = {
2908    "function_declaration": "function",
2909    "generator_function_declaration": "function",
2910    "function_signature": "function",
2911    "class_declaration": "class",
2912    "abstract_class_declaration": "class",
2913    "class": "class",
2914    "interface_declaration": "interface",
2915    "type_alias_declaration": "type",
2916    "enum_declaration": "enum",
2917    "module": "namespace",
2918    "internal_module": "namespace",
2919    "method_definition": "method",
2920    "method_signature": "method",
2921    "abstract_method_signature": "method",
2922    "public_field_definition": "method",
2923}
2924_TS_NAMED_CONTAINERS = {
2925    "class_declaration",
2926    "abstract_class_declaration",
2927    "class",
2928    "interface_declaration",
2929    "module",
2930    "internal_module",
2931    "function_declaration",
2932    "generator_function_declaration",
2933    "method_definition",
2934}
2935
2936
2937def _ts_value_kind(value: TreeSitterNode | None) -> EntityKind | None:
2938    value = _unwrap_ts_value(value)
2939    if value is None:
2940        return None
2941    if value.type == "class":
2942        return "class"
2943    if value.type in TS_FUNCTION_VALUE_TYPES:
2944        return "function"
2945    return None
2946
2947
2948def _ts_declarator(
2949    node: TreeSitterNode,
2950) -> tuple[TreeSitterNode | None, EntityKind | None]:
2951    """The declarator a ``const``/``let``/``var`` is named after, and its kind."""
2952    declarators = [c for c in node.named_children if c.type == "variable_declarator"]
2953    for declarator in declarators:
2954        kind = _ts_value_kind(declarator.child_by_field_name("value"))
2955        if kind is not None:
2956            return declarator, kind
2957    for declarator in declarators:
2958        annotation = declarator.child_by_field_name("type")
2959        if annotation is not None and any(
2960            child.type == "function_type" for child in annotation.named_children
2961        ):
2962            return declarator, "function"
2963    return (declarators[0] if declarators else None), None
2964
2965
2966def _ts_elements(kind: str, name: TreeSitterNode) -> list[SemanticElement]:
2967    """A declaration's name as components: a dotted namespace name (``namespace
2968    A.B``) is a namespace per part; any other name, a string one included, is one."""
2969    text = _collapse(_text(name))
2970    if name.type == "nested_identifier":
2971        return [_element(kind, part) for part in text.split(".")]
2972    return [_element(kind, text)]
2973
2974
2975def _ts_containers(node: TreeSitterNode) -> tuple[list[str], list[SemanticElement]]:
2976    segments: list[str] = []
2977    groups: list[list[SemanticElement]] = []
2978    parent = node.parent
2979    while parent is not None:
2980        if parent.type in _TS_NAMED_CONTAINERS:
2981            name = parent.child_by_field_name("name")
2982            if name is not None:
2983                segments.append(_collapse(_text(name)))
2984                groups.append(_ts_elements(_TS_KINDS[parent.type], name))
2985        elif parent.type == "variable_declarator":
2986            value_kind = _ts_value_kind(parent.child_by_field_name("value"))
2987            if value_kind is not None:
2988                declared = _text(parent.child_by_field_name("name"))
2989                segments.append(declared)
2990                groups.append([_element(value_kind, declared)])
2991        parent = parent.parent
2992    return (
2993        list(reversed(segments)),
2994        [element for group in reversed(groups) for element in group],
2995    )
2996
2997
2998def _ts_describe(node: TreeSitterNode) -> EntityName | None:
2999    prefix, outer = _ts_containers(node)
3000    if node.type in ("lexical_declaration", "variable_declaration"):
3001        declarator, kind = _ts_declarator(node)
3002        if declarator is None or kind is None:
3003            return None
3004        declared = _text(declarator.child_by_field_name("name"))
3005        return _named(
3006            kind, _join(".", prefix, declared), [*outer, _element(kind, declared)]
3007        )
3008    if node.type == "assignment_expression":
3009        kind = _ts_value_kind(node.child_by_field_name("right"))
3010        if kind is None:
3011            return None
3012        # the left-hand side is an expression, not a chain of containers: one component
3013        left = _collapse(_text(node.child_by_field_name("left")))
3014        return _named(kind, _join(".", prefix, left), [*outer, _element(kind, left)])
3015    if node.type == "export_statement":
3016        kind = _ts_value_kind(node.child_by_field_name("value"))
3017        if kind is None:
3018            return None
3019        return _named(
3020            kind, _join(".", prefix, "default"), [*outer, _element(kind, "default")]
3021        )
3022    kind = _TS_KINDS.get(node.type)
3023    if kind is None:
3024        return None
3025    name = node.child_by_field_name("name")
3026    if name is None:
3027        return EntityName(kind, None, None)
3028    return _named(
3029        kind,
3030        _join(".", prefix, _collapse(_text(name))),
3031        [*outer, *_ts_elements(kind, name)],
3032    )
3033
3034
3035# --- Bash, PowerShell, YAML and JSON -----------------------------------------
3036
3037
3038def _bash_describe(node: TreeSitterNode) -> EntityName | None:
3039    if node.type != "function_definition":
3040        return None
3041    own = _text(node.child_by_field_name("name")) or None
3042    return _named("function", own, [_element("function", own or "")])
3043
3044
[docs]3045# @Pester block kinds, IMPL_PS_15, impl, [FE_POWERSHELL]
3046# The entity kind of each Pester block: an ``It`` is a test, a ``Describe`` or a
3047# ``Context`` the group the tests are named in. The one place they are chosen, for the
3048# block and for the blocks its name is joined from.
3049PESTER_BLOCK_KINDS: dict[str, EntityKind] = {
3050    "describe": "namespace",
3051    "context": "namespace",
3052    "it": "test",
3053}
3054
3055_POWERSHELL_KINDS: dict[str, EntityKind] = {
3056    "function_statement": "function",
3057    "class_statement": "class",
3058    "class_method_definition": "method",
3059    "class_property_definition": "property",
3060    "enum_statement": "enum",
3061}
3062
3063
3064def _powershell_child(node: TreeSitterNode, kind: str) -> TreeSitterNode | None:
3065    return next((child for child in node.named_children if child.type == kind), None)
3066
3067
3068# Pester's ``Describe``, ``Context`` and ``It`` parameters, lower-cased, as Pester's
3069# own parameter blocks declare them (``Pending``: Pester 4's ``It``): the switches,
3070# and those that take a value (aliases included: ``Tags``, ``TestCases``). ``-Name``
3071# names the block; any other parameter is undeclared, and may take a value.
3072PESTER_SWITCHES = frozenset({"skip", "allownulloremptyforeach", "pending"})
3073PESTER_VALUE_PARAMETERS = frozenset(
3074    {"tag", "tags", "fixture", "test", "foreach", "testcases"}
3075)
3076PESTER_NAME_PARAMETER = "name"
3077
3078
3079def _attached_value(parameter: TreeSitterNode) -> bool:
3080    """Whether a value is written onto the command parameter, ``-Skip:$false``."""
3081    following = parameter.next_named_sibling
3082    return (
3083        following is not None
3084        and following.type == "command_argument_sep"
3085        and _text(following) == ":"
3086    )
3087
3088
[docs]3089# @Pester block names, IMPL_PS_11, impl, [FE_POWERSHELL]
3090def _pester_name(command: TreeSitterNode) -> str | None:
3091    """A Pester block's name: its ``-Name`` argument, else its first positional
3092    argument past the switches and the parameters with their values that Pester
3093    declares; a string without its delimiters or a bare word, as written. ``None``
3094    when the block is named another way: by an expression, or past a parameter Pester
3095    does not declare."""
3096    elements = _powershell_child(command, "command_elements")
3097    arguments = [
3098        child
3099        for child in (elements.named_children if elements is not None else [])
3100        if child.type != "command_argument_sep"
3101    ]
3102    index = 0
3103    while index < len(arguments):
3104        argument = arguments[index]
3105        if argument.type != "command_parameter":
3106            return _pester_literal(argument)
3107        parameter = _text(argument).lstrip("-").lower()
3108        attached = _attached_value(argument)
3109        if parameter == PESTER_NAME_PARAMETER:
3110            return (
3111                _pester_literal(arguments[index + 1])
3112                if index + 1 < len(arguments)
3113                else None
3114            )
3115        if parameter in PESTER_SWITCHES:
3116            index += 2 if attached else 1
3117        elif parameter in PESTER_VALUE_PARAMETERS:
3118            index += 2
3119            # the grammar splits a bare list (``-Tag a,b``) into ``a`` and ``,b``
3120            while index < len(arguments) and _text(arguments[index]).startswith(","):
3121                index += 1
3122        else:
3123            return None
3124    return None
3125
3126
3127# The expression nodes a command argument is wrapped in, each with one child.
3128_POWERSHELL_ARGUMENT_WRAPPERS = frozenset(
3129    {"array_literal_expression", "unary_expression"}
3130)
3131
3132
3133def _pester_literal(argument: TreeSitterNode) -> str | None:
3134    """The text of a command argument that is a string literal, without its
3135    delimiters, or a bare word; ``None`` for any other argument."""
3136    while (
3137        argument.type in _POWERSHELL_ARGUMENT_WRAPPERS
3138        and argument.named_child_count == 1
3139    ):
3140        argument = argument.named_children[0]
3141    text = _text(argument)
3142    if argument.type == "generic_token":
3143        return text
3144    if argument.type != "string_literal":
3145        return None
3146    if text[:2] in ('@"', "@'") and text[-2:] in ('"@', "'@") and len(text) >= 4:
3147        # a here-string: its content lies between the line breaks after its opening
3148        # and before its closing delimiter
3149        inner = text[2:-2]
3150        inner = inner[2:] if inner.startswith("\r\n") else inner.removeprefix("\n")
3151        inner = inner[:-2] if inner.endswith("\r\n") else inner.removesuffix("\n")
3152        return inner
3153    return text[1:-1] if len(text) >= 2 else text
3154
3155
3156# The scope modifiers a function's name can carry, lower-cased: the function is called
3157# without it (``function global:Get-Foo`` defines ``Get-Foo``).
3158POWERSHELL_SCOPE_MODIFIERS = ("global:", "script:", "private:", "local:")
3159
3160
3161def _powershell_own_name(node: TreeSitterNode) -> str | None:
3162    if node.type == "command":
3163        return _pester_name(node)
3164    if node.type == "function_statement":
[docs]3165        # @PowerShell function names without their scope modifier, IMPL_PS_12, impl, [FE_POWERSHELL]
3166        name = _text(_powershell_child(node, "function_name"))
3167        if name.lower().startswith(POWERSHELL_SCOPE_MODIFIERS):
3168            name = name.split(":", 1)[1]
3169        return name or None
3170    if node.type == "class_property_definition":
3171        return _text(_powershell_child(node, "variable")).lstrip("$") or None
3172    return _text(_powershell_child(node, "simple_name")) or None
3173
3174
3175def _powershell_describe(node: TreeSitterNode) -> EntityName | None:
3176    """A PowerShell function or filter, class, method, property, enum or Pester block.
3177
3178    A method or a property is qualified by its class (``Shape.Area``), a Pester block
3179    by the blocks around it, joined with ``.`` as Pester joins a test's path
3180    (``px.starts``), each named as its source writes it, never expanded; a function, a
3181    class and an enum are named alone. A Pester block in an unnamed one has no name.
3182    """
3183    if node.type == "command":
3184        block = _pester_block(node)
3185        if block is None:
3186            return None
3187        kind: EntityKind = PESTER_BLOCK_KINDS[block]
3188    elif node.type in _POWERSHELL_KINDS:
3189        kind = _POWERSHELL_KINDS[node.type]
3190    else:
3191        return None
3192    own = _powershell_own_name(node)
3193    parent = node.parent
3194    class_name = (
3195        _powershell_own_name(parent)
3196        if parent is not None and parent.type == "class_statement"
3197        else None
3198    )
[docs]3199    # @PowerShell constructors are named after their class, IMPL_PS_13, impl, [FE_POWERSHELL]
3200    # and have no return type: ``[void] Proxy()`` is a method
3201    if (
3202        kind == "method"
3203        and own is not None
3204        and class_name is not None
3205        and own.lower() == class_name.lower()
3206        and _powershell_child(node, "type_literal") is None
3207    ):
3208        kind = "constructor"
3209    if own is None:
3210        return EntityName(kind, None, None)
3211    outer: list[SemanticElement] = []
3212    if kind in ("method", "constructor", "property"):
3213        if parent is not None and parent.type == "class_statement":
3214            if class_name is None:
3215                return EntityName(kind, None, None)
3216            outer.append(_element("class", class_name))
3217    elif node.type == "command":
3218        ancestor = node.parent
3219        while ancestor is not None:
3220            block = _pester_block(ancestor) if ancestor.type == "command" else None
3221            if block is not None:
3222                name = _pester_name(ancestor)
3223                if name is None:
3224                    return EntityName(kind, None, None)
3225                outer.insert(0, _element(PESTER_BLOCK_KINDS[block], name))
3226            ancestor = ancestor.parent
3227    return _named(
3228        kind,
3229        ".".join([*(element["name"] for element in outer), own]),
3230        [*outer, _element(kind, own)],
3231    )
3232
3233
3234# --- VHDL --------------------------------------------------------------------
3235
[docs]3236# @VHDL entity kinds, IMPL_VHDL_8, impl, [FE_VHDL]
3237# The entity kind of each VHDL design element the existing kinds fit: an entity is a
3238# ``module``, its architecture an ``impl`` of it, a package (declaration, body or
3239# instance) a ``namespace``, a component the ``interface`` of the entity it stands for,
3240# a function or a procedure (declared, defined or instantiated) a ``function``. A type
3241# is read by its definition (:data:`_VHDL_TYPE_KINDS`). Every other design element --
3242# a configuration, a context, a process, a block, a generate, an instance, a concurrent
3243# statement, and the data elements (signals, constants, variables, files, aliases,
3244# generics, ports, parameters) -- has no kind: it is named, where it declares one name,
3245# and its name's last component is a ``scope``.
3246_VHDL_KINDS: dict[str, EntityKind] = {
3247    "entity_declaration": "module",
3248    "architecture_definition": "impl",
3249    "package_declaration": "namespace",
3250    "package_definition": "namespace",
3251    "package_instantiation_declaration": "namespace",
3252    "component_declaration": "interface",
3253    "subprogram_declaration": "function",
3254    "subprogram_definition": "function",
3255    "subprogram_instantiation_declaration": "function",
3256    "subtype_declaration": "type",
3257}
3258
3259# A type declaration's kind, by the definition it holds; any other definition (an
3260# integer, a physical, an array, an access, a file type) is a ``type``.
3261_VHDL_TYPE_KINDS: dict[str, EntityKind] = {
3262    "enumeration_type_definition": "enum",
3263    "record_type_definition": "record",
3264    "protected_type_declaration": "class",
3265    "protected_type_body": "class",
3266}
3267
3268# The field that names each design element; the rest are named otherwise (see
3269# _vhdl_own_name).
3270_VHDL_NAME_FIELDS: dict[str, str] = {
3271    "entity_declaration": "entity",
3272    "architecture_definition": "architecture",
3273    "package_declaration": "package",
3274    "package_definition": "package",
3275    "package_instantiation_declaration": "package",
3276    "interface_package_declaration": "package",
3277    "configuration_declaration": "configuration",
3278    "component_declaration": "component",
3279    "type_declaration": "type",
3280    "subtype_declaration": "type",
3281    "interface_type_declaration": "type",
3282}
3283
3284# The nodes a subprogram's name is in, and its field there.
3285_VHDL_SUBPROGRAM_SPECIFICATIONS: dict[str, str] = {
3286    "function_specification": "function",
3287    "procedure_specification": "procedure",
3288    "interface_function_specification": "function",
3289    "interface_procedure_specification": "procedure",
3290}
3291
3292
3293def _vhdl_kind(node: TreeSitterNode) -> EntityKind | None:
3294    if node.type == "type_declaration":
3295        return next(
3296            (
3297                _VHDL_TYPE_KINDS[child.type]
3298                for child in node.named_children
3299                if child.type in _VHDL_TYPE_KINDS
3300            ),
3301            "type",
3302        )
3303    return _VHDL_KINDS.get(node.type)
3304
3305
[docs]3306# @VHDL design element names, IMPL_VHDL_9, impl, [FE_VHDL]
3307def _vhdl_own_name(node: TreeSitterNode) -> str | None:
3308    """A design element's own name, as the source writes it (VHDL is case-insensitive,
3309    and an extended identifier keeps its backslashes, ``\\a b\\``): its declared name,
3310    the label of a statement, or the one name of a declaration of data; ``None`` for
3311    an unlabelled statement and for a declaration of several names (``signal a, b``)."""
3312    field = _VHDL_NAME_FIELDS.get(node.type)
3313    if field is not None:
3314        return _text(node.child_by_field_name(field)) or None
3315    for child in node.named_children:
3316        spec_field = _VHDL_SUBPROGRAM_SPECIFICATIONS.get(child.type)
3317        if spec_field is not None:
3318            return _text(child.child_by_field_name(spec_field)) or None
3319        if child.type == "label_declaration":
3320            return _text(_powershell_child(child, "label")) or None
3321        if child.type == "identifier_list":
3322            names = child.named_children
3323            if len(names) != 1:
3324                return None
3325            return _text(names[0]) or None
3326    if node.type == "subprogram_instantiation_declaration":
3327        designator = node.child_by_field_name("function") or node.child_by_field_name(
3328            "procedure"
3329        )
3330        return _text(designator) or None
3331    if node.type in ("alias_declaration", "context_declaration"):
3332        # the designator, the first child
3333        children = node.named_children
3334        return (_text(children[0]) or None) if children else None
3335    return None
3336
3337
3338def _vhdl_components(node: TreeSitterNode) -> list[SemanticElement] | None:
3339    """The components ``node`` stands for: an architecture is an ``impl`` of the
3340    ``module`` its ``of`` clause names (``counter.rtl``); ``None`` without a name."""
3341    own = _vhdl_own_name(node)
3342    if own is None:
3343        return None
3344    element = _element(_vhdl_kind(node) or "scope", own)
3345    if node.type == "architecture_definition":
3346        entity = _text(node.child_by_field_name("entity"))
3347        if not entity:
3348            return None
3349        return [_element("module", entity), element]
3350    return [element]
3351
3352
3353def _vhdl_describe(node: TreeSitterNode) -> EntityName | None:
3354    """A VHDL design element, named by the design elements around it, joined with
3355    ``.`` as VHDL's expanded names are (``pkg.add``, ``counter.rtl.p_main``).
3356
3357    An architecture is named under the entity it implements (``counter.rtl``), and so
3358    is what it holds. A design element inside an unnamed one (an unlabelled process,
3359    a declaration of several names) has no name. See :data:`_VHDL_KINDS` for the
3360    kinds.
3361    """
3362    if not is_entity(node, CommentType.vhdl):
3363        return None
3364    kind = _vhdl_kind(node)
3365    own = _vhdl_components(node)
3366    if own is None:
3367        return EntityName(kind, None, None)
3368    outer: list[SemanticElement] = []
3369    ancestor = node.parent
3370    while ancestor is not None:
3371        if is_entity(ancestor, CommentType.vhdl):
3372            components = _vhdl_components(ancestor)
3373            if components is None:
3374                return EntityName(kind, None, None)
3375            outer[:0] = components
3376        ancestor = ancestor.parent
3377    semantic = [*outer, *own]
3378    return _named(kind, ".".join(element["name"] for element in semantic), semantic)
3379
3380
3381def _data_describe(node: TreeSitterNode) -> EntityName:
3382    """A YAML or JSON entry: a key/value pair is a ``key``, anything else an ``item``."""
3383    is_pair = node.type in ("block_mapping_pair", "flow_pair", "pair")
3384    return EntityName("key" if is_pair else "item", None, None)
3385
3386
3387_DESCRIBERS: dict[CommentType, Callable[[TreeSitterNode], EntityName | None]] = {
3388    CommentType.cpp: _cpp_describe,
3389    CommentType.python: _python_describe,
3390    CommentType.rust: _rust_describe,
3391    CommentType.cs: _csharp_describe,
3392    CommentType.go: _go_describe,
3393    CommentType.ts: _ts_describe,
3394    CommentType.bash: _bash_describe,
3395    CommentType.powershell: _powershell_describe,
3396    CommentType.vhdl: _vhdl_describe,
3397    CommentType.yaml: _data_describe,
3398    CommentType.jsonc: _data_describe,
3399}
3400
3401
3402def describe_entity(
3403    node: TreeSitterNode, comment_type: CommentType, *, source: bytes | None = None
3404) -> EntityName | None:
3405    """Name the definition ``node``: its kind, its in-file qualified name, and the
3406    components of that name.
3407
3408    Pure: it reads the node and its ancestors (and, for a C# file-scoped namespace, the
3409    file's top-level siblings), never the file system.
3410
3411    :param node: A definition node, as association binds it.
3412    :param comment_type: The language of the file ``node`` was parsed from.
3413    :param source: The file as written, which a C or C++ name is read from when given
3414        (its tree may be parsed from a prepared copy); else the tree's own text.
3415    :return: The name, or ``None`` if ``node`` is no entity this function can name.
3416    """
3417    if comment_type == CommentType.cpp:
3418        return _cpp_describe_in(node, source, _ALONE)
3419    describer = _DESCRIBERS.get(comment_type)
3420    return describer(node) if describer is not None else None
3421
3422
3423# --- Rendering the components ------------------------------------------------
3424
3425
3426def _rust_is_path(text: str) -> bool:
3427    """``text`` is a type rustc writes without brackets in a path: a name, a path or a
3428    primitive, with any generic arguments (``Widget``, ``a::Widget<dyn Fn() -> u8>``,
3429    ``u32``) -- not ``dyn Shape``, ``&T``, ``[T]``, a tuple or a function type."""
3430    if text and not (_is_ident_char(text[0]) or text[0] == ":"):
3431        return False
3432    depth = 0
3433    previous = ""
3434    for char in text:
3435        if char in "<([":
3436            depth += 1
3437        elif depth and (char in ")]" or (char == ">" and previous != "-")):
3438            depth -= 1
3439        elif depth == 0 and not (_is_ident_char(char) or char == ":"):
3440            return False
3441        previous = char
3442    return True
3443
3444
3445# The types tree-sitter-rust reads as ``primitive_type``: never prefixed with a module.
3446_RUST_PRIMITIVES = frozenset(
3447    {
3448        "bool",
3449        "char",
3450        "str",
3451        "u8",
3452        "i8",
3453        "u16",
3454        "i16",
3455        "u32",
3456        "i32",
3457        "u64",
3458        "i64",
3459        "u128",
3460        "i128",
3461        "usize",
3462        "isize",
3463        "f32",
3464        "f64",
3465    }
3466)
3467
3468
3469def _rust_resolves_in_module(name: str) -> bool:
3470    """``name`` is a type written bare, which the qualified name reads as one of the
3471    modules it is written in: a name, with any generic arguments (``G``,
3472    ``Vec<T>``), not a path, a primitive or a type that is no path."""
3473    if not name or not (name[0].isalpha() or name[0] == "_") or "::" in name:
3474        return False
3475    return _rust_is_path(name) and name.split("<", 1)[0] not in _RUST_PRIMITIVES
3476
3477
3478def _rust_render(semantic: Sequence[SemanticElement]) -> str:
3479    """Rust's qualified name: the components joined with ``::``, an impl written as
3480    ``<Widget as Shape>`` for a trait impl and ``<dyn Shape>`` for a self type that is
3481    no path, and the modules before an impl folded into its self type when it is
3482    written bare (``m::G``) -- dropped otherwise, as the qualified name has no place
3483    for them (``x::P``, a path from the crate root without its ``crate::``)."""
3484    impl_at = next(
3485        (index for index, element in enumerate(semantic) if element["kind"] == "impl"),
3486        None,
3487    )
3488    if impl_at is None:
3489        return "::".join(element["name"] for element in semantic)
3490    modules = [element["name"] for element in semantic[:impl_at]]
3491    impl = semantic[impl_at]
3492    self_type = impl["name"]
3493    if modules and _rust_resolves_in_module(self_type):
3494        self_type = "::".join([*modules, self_type])
3495    else:
3496        self_type = self_type.removeprefix("crate::")
3497    if "trait" in impl:
3498        segment = f"<{self_type} as {impl['trait']}>"
3499    elif _rust_is_path(self_type):
3500        segment = self_type
3501    else:
3502        segment = f"<{self_type}>"
3503    return "::".join(
3504        [segment, *(element["name"] for element in semantic[impl_at + 1 :])]
3505    )
3506
3507
3508def _python_locals_after(semantic: Sequence[SemanticElement], index: int) -> bool:
3509    """CPython writes ``<locals>`` after the component at ``index``: a function or a
3510    method that another component follows."""
3511    return (
3512        semantic[index]["kind"] in ("function", "method") and index < len(semantic) - 1
3513    )
3514
3515
3516def render_semantic(
3517    semantic: Sequence[SemanticElement] | None, language: str
3518) -> str | None:
3519    """Spell ``semantic`` as its language writes a qualified name: the entity's
3520    ``qualified_name``.
3521
3522    C and C++ join the components with ``::``. Rust does too, writes an ``impl`` as
3523    ``<Widget as Shape>`` for a trait impl and ``<dyn Shape>`` for a self type that is
3524    no path, and folds the modules before an impl into a self type written bare
3525    (``m::G``). Python joins them with ``.``, with ``<locals>`` after a function or a
3526    method another component follows (CPython's ``__qualname__``). C#, Go,
3527    TypeScript, JavaScript, Bash, PowerShell and VHDL join them with ``.``.
3528
3529    :param semantic: An entity's components, as :func:`describe_entity` gives them.
3530    :param language: The language of the entity's file, as ``scope.language``
3531        records it (:func:`scope_language`).
3532    :return: The qualified name; ``None`` for no components.
3533    """
3534    if semantic is None:
3535        return None
3536    if language in ("c", "cpp"):
3537        return "::".join(element["name"] for element in semantic)
3538    if language == "rust":
3539        return _rust_render(semantic)
3540    if language == "python":
3541        parts: list[str] = []
3542        for index, element in enumerate(semantic):
3543            parts.append(element["name"])
3544            if _python_locals_after(semantic, index):
3545                parts.append("<locals>")
3546        return ".".join(parts)
3547    return ".".join(element["name"] for element in semantic)
3548
3549
3550def name_chain(
3551    semantic: Sequence[SemanticElement] | None, language: str
3552) -> tuple[tuple[tuple[str, str], ...], ...] | None:
3553    """What identifies an entity in its file: the identity-bearing part of its
3554    rendering -- its components' names and other keys (a Rust impl's ``trait``), and
3555    in Python the ``<locals>`` boundaries -- without their kinds.
3556
3557    Kinds describe, names identify. A kind is a best-effort classification, for
3558    filtering and rendering, and one entity's kinds depend on how it is written: a
3559    method defined in its class is ``namespace ns``, ``class W``, ``method f``, the
3560    same method defined out of line ``scope ns``, ``class W``, ``method f``. So two
3561    entities are the same, or one is inside the other, by their name chains, never
3562    by their kinds. Where a kind does show in the name, so does the chain: CPython's
3563    ``<locals>`` after a function or a method (``A.f.<locals>.g`` is not ``A.f.g``) is
3564    a link of its own, ``(("name", "<locals>"),)``, which no Python name can be.
3565
3566    A ``qualified_name`` key is left out too: a component rebuilt from a code hop's
3567    semantic entry carries one on the entity, the rendering of this very chain, which
3568    adds no identity and would break containment (a class's chain is a prefix of its
3569    method's only without it).
3570
3571    :param semantic: An entity's components (``scope.semantic``).
3572    :param language: The entity's file's language (``scope.language``).
3573    :return: One tuple of ``(key, value)`` pairs per component, its keys sorted,
3574        ``kind`` and ``qualified_name`` left out, and a ``<locals>`` link where Python
3575        writes one; ``None`` for no components.
3576    """
3577    if semantic is None:
3578        return None
3579    chain: list[tuple[tuple[str, str], ...]] = []
3580    for index, element in enumerate(semantic):
3581        chain.append(
3582            tuple(
3583                sorted(
3584                    (key, str(value))
3585                    for key, value in element.items()
3586                    if key not in ("kind", "qualified_name")
3587                )
3588            )
3589        )
3590        if language == "python" and _python_locals_after(semantic, index):
3591            chain.append((("name", "<locals>"),))
3592    return tuple(chain)
3593
3594
3595# The suffixes of the ``ts`` comment type that are JavaScript, in lower case.
3596_JAVASCRIPT_SUFFIXES = frozenset({".js", ".jsx", ".mjs", ".cjs"})
3597
3598
3599def scope_language(comment_type: CommentType, path: PurePath) -> str:
3600    """A source file's language, by its name (``scope.language``), spelled as editors'
3601    language identifiers spell it.
3602
3603    The comment type gives the language family and the grammar; the suffix then picks
3604    ``c`` among C and C++ and ``javascript`` among the ``ts`` type's files, and the
3605    comment type alone names a file of any other suffix (a ``.py`` file in a C++
3606    project is ``cpp``). So: ``c`` for a ``.c`` file and ``cpp`` for every other C and
3607    C++ file (a ``.h`` header included: its name does not say whether it is C or C++,
3608    and it is analysed as C++; and a ``.C`` file, which compilers read as C++);
3609    ``csharp``; ``javascript`` for a ``.js``, ``.jsx``, ``.mjs`` or ``.cjs`` file, in
3610    any case, and ``typescript`` for every other file of the ``ts`` comment type,
3611    ``.tsx`` included; otherwise the comment type (``python``, ``rust``, ``go``,
3612    ``yaml``, ``jsonc``, ``bash``, ``powershell``, ``vhdl``). A ``.c`` file is parsed with the C++ grammar too,
3613    and its components render as C++'s.
3614
3615    :param comment_type: The project's comment type.
3616    :param path: The file's path; only its suffix is read, case included for C (discovery
3617        reads it in any case, so a ``.C`` file is traced, as C++).
3618    """
3619    if comment_type == CommentType.cpp:
3620        return "c" if path.suffix == ".c" else "cpp"
3621    if comment_type == CommentType.cs:
3622        return "csharp"
3623    if comment_type == CommentType.ts:
3624        if path.suffix.lower() in _JAVASCRIPT_SUFFIXES:
3625            return "javascript"
3626        return "typescript"
3627    return comment_type.value
3628
3629
3630def code_scope(
3631    association: Association,
3632    comment_type: CommentType,
3633    language: str,
3634    names: CppNames | None = None,
3635) -> CodeScope:
3636    """The ``scope`` object of an analysis record, for ``association`` in a file of
3637    ``language`` (:func:`scope_language`).
3638
3639    ``names`` is what the whole C++ file decides about its names (:class:`CppNames`),
3640    for a C++ file the analysis parsed; the entity is named as the file names it.
3641    """
3642    node = association.node
3643    if names is None:
3644        name = describe_entity(node, comment_type)
3645    else:
3646        name = describe_entity(node, comment_type, source=names.source)
3647        if name is not None:
3648            name = names.qualify(name, node.start_point.row + 1)
3649    return {
3650        "kind": node.type,
3651        "start": node.start_point.row + 1,
3652        "end": node.end_point.row + 1,
3653        "language": language,
3654        "entity_kind": name.kind if name is not None else None,
3655        "qualified_name": name.qualified_name if name is not None else None,
3656        "semantic": (
3657            list(name.semantic)
3658            if name is not None and name.semantic is not None
3659            else None
3660        ),
3661        "mechanism": association.mechanism,
3662    }