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 }