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