codebase-explorer 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. cbe/__init__.py +9 -0
  2. cbe/__main__.py +4 -0
  3. cbe/aggregation.py +183 -0
  4. cbe/api_completion.py +410 -0
  5. cbe/batching.py +283 -0
  6. cbe/catalog_query.py +376 -0
  7. cbe/checks.py +390 -0
  8. cbe/cli.py +434 -0
  9. cbe/concrete_facts.py +321 -0
  10. cbe/data/LICENSE.tiktoken +30 -0
  11. cbe/data/o200k_base.tiktoken +199998 -0
  12. cbe/declarations.py +537 -0
  13. cbe/docs.py +929 -0
  14. cbe/generate.py +316 -0
  15. cbe/grammars.py +741 -0
  16. cbe/graph.py +857 -0
  17. cbe/graph_backends/__init__.py +43 -0
  18. cbe/graph_backends/_calls.py +262 -0
  19. cbe/graph_backends/_paths.py +42 -0
  20. cbe/graph_backends/identity.py +93 -0
  21. cbe/graph_backends/imports.py +256 -0
  22. cbe/graph_backends/joern.py +1021 -0
  23. cbe/graph_backends/resolution.py +288 -0
  24. cbe/graph_backends/toolchain.py +396 -0
  25. cbe/grounding.py +57 -0
  26. cbe/host_run.py +1752 -0
  27. cbe/model_settings.py +80 -0
  28. cbe/projection.py +305 -0
  29. cbe/prompts.py +316 -0
  30. cbe/records.py +200 -0
  31. cbe/skeleton.py +474 -0
  32. cbe/source_context.py +709 -0
  33. cbe/symbols.py +136 -0
  34. cbe/token_budget.py +68 -0
  35. cbe/treename.py +398 -0
  36. cbe/usage.py +300 -0
  37. cbe_install/__init__.py +11 -0
  38. cbe_install/cli.py +434 -0
  39. cbe_install/resources/agents/cbe-writer.md +16 -0
  40. cbe_install/resources/codebase-explorer/SKILL.md +101 -0
  41. cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
  42. cbe_install/resources/codebase-explorer/references/codex.md +37 -0
  43. codebase_explorer-2.2.0.dist-info/METADATA +233 -0
  44. codebase_explorer-2.2.0.dist-info/RECORD +47 -0
  45. codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
  46. codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
  47. codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
cbe/declarations.py ADDED
@@ -0,0 +1,537 @@
1
+ """Extract declarations and searchable signatures from syntax trees."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from pathlib import PurePosixPath
7
+ from typing import Any
8
+
9
+ from cbe.grammars import (
10
+ GrammarSpec,
11
+ OffsetMap,
12
+ _all_specs,
13
+ _make_parser,
14
+ canonical_symbol_id,
15
+ )
16
+
17
+
18
+ _IDENTIFIER_TYPES = frozenset(
19
+ {
20
+ "identifier",
21
+ "type_identifier",
22
+ "field_identifier",
23
+ "property_identifier",
24
+ "private_property_identifier",
25
+ "namespace_identifier",
26
+ "package_identifier",
27
+ "constant",
28
+ "operator_name",
29
+ "destructor_name",
30
+ "scoped_identifier",
31
+ "simple_identifier",
32
+ }
33
+ )
34
+
35
+ _SIGNATURE_LIMIT = 400
36
+
37
+
38
+ @dataclass
39
+ class _Declaration:
40
+ node: Any
41
+ kind: str
42
+ name: str
43
+ qualified_name: str
44
+ parent_id: str | None
45
+ symbol_id: str
46
+ start_byte: int
47
+ end_byte: int
48
+ children: list["_Declaration"] = field(default_factory=list)
49
+ ancestors: tuple[str, ...] = ()
50
+ signature: str = ""
51
+ exclusive_end_byte: int | None = None
52
+
53
+
54
+ def _text(offsets: OffsetMap, start_byte: int, end_byte: int) -> str:
55
+ return offsets.raw[start_byte:end_byte].decode("utf-8", errors="replace")
56
+
57
+
58
+ def _node_name(offsets: OffsetMap, node: Any) -> str:
59
+ # Field lookup first: most grammars expose the declared name as ``name``.
60
+ name_node = node.child_by_field_name("name")
61
+ if name_node is not None:
62
+ return _text(offsets, name_node.start_byte, name_node.end_byte)
63
+ # C/C++ function declarators carry the name, not a ``name`` field. This
64
+ # must run before scanning children, otherwise the return type is picked.
65
+ declarator = node.child_by_field_name("declarator")
66
+ if declarator is not None:
67
+ found = _first_identifier(offsets, declarator)
68
+ if found:
69
+ return found
70
+ for child in node.named_children:
71
+ if child.type in _IDENTIFIER_TYPES:
72
+ return _text(offsets, child.start_byte, child.end_byte)
73
+ # Go grouped ``type`` declarations nest the name in ``type_spec``.
74
+ for child in node.named_children:
75
+ if child.type in {"type_spec", "function_declarator", "init_declarator"}:
76
+ found = _node_name(offsets, child)
77
+ if found:
78
+ return found
79
+ return ""
80
+
81
+
82
+ def _first_identifier(offsets: OffsetMap, node: Any) -> str:
83
+ for child in node.named_children:
84
+ if child.type in _IDENTIFIER_TYPES or child.type in {"operator_name", "destructor_name"}:
85
+ return _text(offsets, child.start_byte, child.end_byte)
86
+ found = _first_identifier(offsets, child)
87
+ if found:
88
+ return found
89
+ return ""
90
+
91
+
92
+ def _body_start(node: Any) -> int:
93
+ body = node.child_by_field_name("body")
94
+ return body.start_byte if body is not None else -1
95
+
96
+
97
+ def _declarator_name(offsets: OffsetMap, node: Any, *, type_alias: bool = False) -> str:
98
+ if node.type == "identifier" or (type_alias and node.type == "type_identifier"):
99
+ return _text(offsets, node.start_byte, node.end_byte)
100
+ if node.type == "function_declarator":
101
+ child = node.child_by_field_name("declarator")
102
+ if not type_alias:
103
+ # Parentheses alone do not make a function a variable: the
104
+ # innermost name must be reached through a pointer or reference.
105
+ if child is None or child.type != "parenthesized_declarator":
106
+ return ""
107
+ inner = [part for part in child.named_children if part.type != "comment"]
108
+ if len(inner) != 1 or inner[0].type not in {"pointer_declarator", "reference_declarator"}:
109
+ return ""
110
+ elif node.type in {"init_declarator", "array_declarator", "pointer_declarator", "reference_declarator"}:
111
+ child = node.child_by_field_name("declarator")
112
+ if child is None and node.type == "reference_declarator":
113
+ child = next(iter(node.named_children), None)
114
+ elif node.type == "parenthesized_declarator":
115
+ parts = [part for part in node.named_children if part.type != "comment"]
116
+ child = parts[0] if len(parts) == 1 else None
117
+ else:
118
+ return ""
119
+ return _declarator_name(offsets, child, type_alias=type_alias) if child is not None else ""
120
+
121
+
122
+ def _binding_names(offsets: OffsetMap, node: Any) -> list[str]:
123
+ if node.type in {"identifier", "shorthand_property_identifier_pattern"}:
124
+ return [_text(offsets, node.start_byte, node.end_byte)]
125
+ if node.type in {"pair_pattern", "assignment_pattern", "object_assignment_pattern"}:
126
+ field = "value" if node.type == "pair_pattern" else "left"
127
+ binding = node.child_by_field_name(field)
128
+ return _binding_names(offsets, binding) if binding is not None else []
129
+ if node.type in {"object_pattern", "array_pattern", "rest_pattern"}:
130
+ return [name for child in node.named_children for name in _binding_names(offsets, child)]
131
+ return []
132
+
133
+
134
+ def _contains_node(node: Any, kinds: set[str]) -> bool:
135
+ return node.type in kinds or any(_contains_node(child, kinds) for child in node.named_children)
136
+
137
+
138
+ def _c_declaration_scope(node: Any) -> bool:
139
+ child = node
140
+ ancestor = node.parent
141
+ while ancestor is not None:
142
+ if ancestor.type == "declaration_list":
143
+ if ancestor.parent is None or ancestor.parent.type not in {"namespace_definition", "linkage_specification"}:
144
+ return False
145
+ elif ancestor.type == "template_declaration":
146
+ payload = [part for part in ancestor.named_children
147
+ if part.type not in {"template_parameter_list", "requires_clause", "comment"}]
148
+ if payload != [child]:
149
+ return False
150
+ elif ancestor.type not in {"translation_unit", "preproc_if", "preproc_ifdef", "preproc_else",
151
+ "preproc_elif", "namespace_definition", "linkage_specification"}:
152
+ return False
153
+ child, ancestor = ancestor, ancestor.parent
154
+ return True
155
+
156
+
157
+ def _ordinary_name(spec: GrammarSpec, offsets: OffsetMap, node: Any, scope: str | None) -> str:
158
+ if node.type not in spec.ordinary_declaration_kinds:
159
+ return ""
160
+ if spec.c_declaration_rules and node.type in {"preproc_def", "preproc_function_def"}:
161
+ name = node.child_by_field_name("name")
162
+ return _text(offsets, name.start_byte, name.end_byte) if name is not None and not node.has_error else ""
163
+ if scope == "function" or (scope == "class" and not spec.ordinary_in_class_scope):
164
+ return ""
165
+ if spec.c_declaration_rules and not _c_declaration_scope(node):
166
+ return ""
167
+ if spec.c_declaration_rules and node.has_error:
168
+ # Recovery can split a macro-prefixed function into a fake variable
169
+ # followed by its prototype, or even misidentify a macro's own name.
170
+ return ""
171
+ if spec.ordinary_name_style == "fields":
172
+ return ", ".join(_node_name(offsets, child) for child in node.named_children
173
+ if child.type == "variable_declarator")
174
+ if spec.ordinary_name_style == "constants":
175
+ left = node.child_by_field_name("left")
176
+ if left is not None and left.type in {"constant", "scope_resolution"}:
177
+ return _text(offsets, left.start_byte, left.end_byte)
178
+ return ""
179
+ if spec.ordinary_name_style == "python":
180
+ left = node.child_by_field_name("left")
181
+ statement = node.parent
182
+ container = statement.parent if statement is not None and statement.type == "expression_statement" else None
183
+ if (left is None or left.type != "identifier" or container is None
184
+ or not (container.type == "module"
185
+ or (container.type == "block" and container.parent is not None
186
+ and container.parent.type == "class_definition"))):
187
+ return ""
188
+ return _text(offsets, left.start_byte, left.end_byte)
189
+ ancestor = node.parent
190
+ while ancestor is not None:
191
+ if ancestor.type in {"statement_block", "compound_statement", "block", "for_statement", "if_statement"}:
192
+ return ""
193
+ ancestor = ancestor.parent
194
+ if spec.ordinary_name_style == "bindings":
195
+ ancestor = node.parent
196
+ while ancestor is not None:
197
+ if ancestor.type not in {"lexical_declaration", "variable_declaration", "export_statement",
198
+ "ambient_declaration", "declare_statement", "program"}:
199
+ return ""
200
+ ancestor = ancestor.parent
201
+ siblings = list(node.parent.named_children)
202
+ index = siblings.index(node)
203
+ if any(child.type in spec.binding_kinds
204
+ and (value := child.child_by_field_name("value")) is not None
205
+ and value.type in spec.value_function_kinds for child in siblings[index + 1:]):
206
+ return "" # Preserve the prefix owned by the existing first function binding.
207
+ binding = node.child_by_field_name("name")
208
+ return ", ".join(_binding_names(offsets, binding)) if binding is not None else ""
209
+ if spec.ordinary_name_style == "grouped_names":
210
+ def names_in(part: Any) -> list[str]:
211
+ if part.type in {"var_spec", "const_spec"}:
212
+ return [_text(offsets, child.start_byte, child.end_byte)
213
+ for child in part.children_by_field_name("name")]
214
+ return [name for child in part.named_children for name in names_in(child)]
215
+ return ", ".join(names_in(node))
216
+ if spec.c_declaration_rules and node.type in {"declaration", "type_definition"}:
217
+ declared_type = node.child_by_field_name("type")
218
+ if (declared_type is not None and _text(offsets, declared_type.start_byte, declared_type.end_byte)
219
+ in spec.recovery_type_keywords):
220
+ return "" # C++ keywords can look like C-style variable types during recovery.
221
+ if node.type == "declaration" and _contains_node(node, {"function_definition", "compound_statement"}):
222
+ return ""
223
+ names = []
224
+ for declarator in node.children_by_field_name("declarator"):
225
+ name = _declarator_name(offsets, declarator, type_alias=node.type == "type_definition")
226
+ if not name:
227
+ return "" # Do not publish a mixed variable/function declaration either.
228
+ names.append(name)
229
+ return ", ".join(names)
230
+ names = node.children_by_field_name("name")
231
+ return ", ".join(_text(offsets, child.start_byte, child.end_byte) for child in names)
232
+
233
+
234
+ class _DeclarationExtractor:
235
+ def __init__(self, spec: GrammarSpec, offsets: OffsetMap, root: Any, path: str) -> None:
236
+ self.spec = spec
237
+ self.offsets = offsets
238
+ self.root = root
239
+ self.path = path
240
+ self.declarations: list[_Declaration] = []
241
+ # C recovery may flatten C++ locals and fields directly into a header's
242
+ # translation unit. A second tree only vetoes uncertain ordinary regions;
243
+ # existing functions/types continue to use the original tree and identities.
244
+ self.check_header_scope = spec.name == "c" and PurePosixPath(path).suffix.lower() == ".h" and root.has_error
245
+ self.header_root = None
246
+ self.cpp_spec = next((item for item in _all_specs() if item.name == "cpp"), None)
247
+ if self.check_header_scope and self.cpp_spec is not None:
248
+ try:
249
+ self.header_root = _make_parser(self.cpp_spec).parse(offsets.raw).root_node
250
+ except (ImportError, AttributeError):
251
+ pass # Without scope evidence, leave uncertain declarations residual.
252
+
253
+ def header_scope_matches(self, node: Any, name: str) -> bool:
254
+ if not self.check_header_scope or node.type in {"preproc_def", "preproc_function_def"}:
255
+ return True
256
+ if self.header_root is None:
257
+ return False
258
+ # Anchor each declared identifier, rather than the whole declaration:
259
+ # C recovery can give namespace declarations different spans/scopes.
260
+ def identifiers(part: Any) -> list[Any]:
261
+ if part.type in {"identifier", "type_identifier"}:
262
+ return [part]
263
+ return [item for child in part.named_children for item in identifiers(child)]
264
+
265
+ anchors = identifiers(node)
266
+ for declared_name in name.split(", "):
267
+ anchor = next((item for item in anchors
268
+ if _text(self.offsets, item.start_byte, item.end_byte) == declared_name), None)
269
+ if anchor is None:
270
+ return False
271
+ counterpart = self.header_root.descendant_for_byte_range(anchor.start_byte, anchor.end_byte - 1)
272
+ while counterpart is not None and counterpart.type not in {"declaration", "alias_declaration", "type_definition"}:
273
+ if counterpart.type in {"parameter_list", "compound_statement", "field_declaration_list", "ERROR"}:
274
+ return False
275
+ counterpart = counterpart.parent
276
+ if (counterpart is None
277
+ or declared_name not in _ordinary_name(
278
+ self.cpp_spec, self.offsets, counterpart, None).split(", ")):
279
+ return False
280
+ return True
281
+
282
+ def extract(self) -> list[_Declaration]:
283
+ self.visit(self.root, None, None, [None])
284
+ declarations = [decl for decl in self.declarations
285
+ if decl.kind != "module_region" or not any(
286
+ other is not decl and decl.start_byte <= other.start_byte
287
+ and other.end_byte <= decl.end_byte for other in self.declarations)]
288
+ return _coalesce_declarations(self.offsets, declarations)
289
+
290
+ def visit(
291
+ self,
292
+ node: Any,
293
+ parent: _Declaration | None,
294
+ scope: str | None,
295
+ prefix: list[int | None],
296
+ ) -> None:
297
+ ntype = node.type
298
+ if ntype in self.spec.wrappers:
299
+ # A wrapper prefix (decorators, ``export``, ``template`` ...) belongs
300
+ # to the first declaration the wrapper contains. Only that first
301
+ # declaration consumes it, so sibling declarators in a multi-binding
302
+ # statement never copy the wrapper start and never overlap.
303
+ start = prefix[0] if prefix[0] is not None else node.start_byte
304
+ holder: list[int | None] = [start]
305
+ for child in node.named_children:
306
+ self.visit(child, parent, scope, holder)
307
+ # Nested wrappers share ownership of the prefix. Once any inner
308
+ # declaration consumes it, later siblings of every outer wrapper
309
+ # must start at their own node, not at that outer wrapper's start.
310
+ if holder[0] is None:
311
+ prefix[0] = None
312
+ return
313
+
314
+ if self.spec.grouped_type_declarations and ntype == "type_declaration" and scope != "function":
315
+ self.visit_go_type(node, parent, prefix)
316
+ return
317
+
318
+ if ntype in self.spec.class_kinds:
319
+ if not _is_type_definition(self.spec, node):
320
+ # C/C++ type reference or forward declaration: not a chunk
321
+ # boundary, but still descend in case of nested definitions.
322
+ for child in node.named_children:
323
+ self.visit(child, parent, scope, prefix)
324
+ return
325
+ decl = _make_declaration(
326
+ self.offsets, node, self.path, parent, self.spec.class_kinds[ntype], prefix[0])
327
+ prefix[0] = None
328
+ self.declarations.append(decl)
329
+ if parent is not None:
330
+ parent.children.append(decl)
331
+ child_scope = scope if decl.kind in {"namespace", "module"} or self.spec.preserve_type_scope else "class"
332
+ for child in node.named_children:
333
+ self.visit(child, decl, child_scope, [None])
334
+ return
335
+
336
+ if ntype in self.spec.function_kinds:
337
+ declared = self.spec.function_kinds[ntype]
338
+ kind = "method" if declared == "function" and scope == "class" else declared
339
+ decl = _make_declaration(self.offsets, node, self.path, parent, kind, prefix[0])
340
+ prefix[0] = None
341
+ self.declarations.append(decl)
342
+ if parent is not None:
343
+ parent.children.append(decl)
344
+ for child in node.named_children:
345
+ self.visit(child, decl, "function", [None])
346
+ return
347
+
348
+ if ntype in self.spec.binding_kinds:
349
+ value = node.child_by_field_name("value")
350
+ if value is not None and value.type in self.spec.value_function_kinds:
351
+ kind = "method" if scope == "class" else "function"
352
+ decl = _make_declaration(self.offsets, node, self.path, parent, kind, prefix[0])
353
+ prefix[0] = None
354
+ self.declarations.append(decl)
355
+ if parent is not None:
356
+ parent.children.append(decl)
357
+ self.visit(value, decl, "function", [None])
358
+ return
359
+ # Function bindings retain their established extraction path.
360
+ self.visit_ordinary(node, parent, scope, prefix)
361
+
362
+ def visit_go_type(self, node: Any, parent: _Declaration | None, prefix: list[int | None]) -> None:
363
+ specs = [child for child in node.named_children if child.type in {"type_spec", "type_alias"}]
364
+ legacy = _make_declaration(self.offsets, node, self.path, parent, "type", prefix[0])
365
+ prefix[0] = None
366
+ for index, child in enumerate(specs):
367
+ if index == 0:
368
+ # Keep the existing group's canonical identity and full span.
369
+ # Old grammars' alias-only groups may have no discovered name.
370
+ owns_first = legacy.name == _node_name(self.offsets, child)
371
+ legacy.exclusive_end_byte = (child.end_byte if owns_first else child.start_byte)
372
+ if len(specs) == 1 and owns_first:
373
+ legacy.exclusive_end_byte = None
374
+ self.declarations.append(legacy)
375
+ if parent is not None:
376
+ parent.children.append(legacy)
377
+ if owns_first:
378
+ continue
379
+ decl = _make_declaration(self.offsets, child, self.path, parent, "module_region", None)
380
+ self.declarations.append(decl)
381
+ if parent is not None:
382
+ parent.children.append(decl)
383
+
384
+ def visit_ordinary(
385
+ self, node: Any, parent: _Declaration | None, scope: str | None, prefix: list[int | None]
386
+ ) -> None:
387
+ ntype = node.type
388
+ name = _ordinary_name(self.spec, self.offsets, node, scope)
389
+ if self.check_header_scope and not name and ntype in {"declaration", "type_definition"}:
390
+ ancestor = node.parent
391
+ while ancestor is not None and ancestor.type != "function_definition":
392
+ ancestor = ancestor.parent
393
+ declared_type = ancestor.child_by_field_name("type") if ancestor is not None else None
394
+ if declared_type is not None and _text(
395
+ self.offsets, declared_type.start_byte, declared_type.end_byte) == "namespace":
396
+ # C calls a namespace a function and can put ERROR in its using
397
+ # aliases. Confirm these candidates in C++, retaining C vetoes elsewhere.
398
+ names = [_declarator_name(self.offsets, child, type_alias=ntype == "type_definition")
399
+ for child in node.children_by_field_name("declarator")]
400
+ name = ", ".join(names) if names and all(names) else ""
401
+ if name and not self.header_scope_matches(node, name):
402
+ name = ""
403
+ if name and ntype == "type_definition":
404
+ defined_type = node.child_by_field_name("type")
405
+ if (defined_type is not None and defined_type.type in self.spec.class_kinds
406
+ and _is_type_definition(self.spec, defined_type)):
407
+ # The existing type owns the definition, with its original id
408
+ # and full span. The typedef adds searchable aliases, no unit.
409
+ first = len(self.declarations)
410
+ for child in node.named_children:
411
+ self.visit(child, parent, scope, [None])
412
+ for declaration in self.declarations[first:]:
413
+ if declaration.node == defined_type:
414
+ declaration.signature += "\n" + name
415
+ return
416
+ if name:
417
+ # Discover children before committing the ordinary region. A value
418
+ # must not wrap another unit or consume its wrapper prefix.
419
+ first = len(self.declarations)
420
+ holder = [prefix[0]]
421
+ for child in node.named_children:
422
+ self.visit(child, parent, scope, holder)
423
+ if len(self.declarations) != first:
424
+ prefix[0] = holder[0]
425
+ return
426
+ decl = _make_declaration(self.offsets, node, self.path, parent, "module_region", prefix[0],
427
+ name=name.split(", ", 1)[0])
428
+ # Search metadata holds every name, without duplicating the list in
429
+ # inventory names and canonical ids. Do not truncate long runs.
430
+ decl.signature = name
431
+ prefix[0] = None
432
+ if node.type == "variable_declarator" and node == node.parent.named_children[-1]:
433
+ decl.end_byte = node.parent.end_byte
434
+ self.declarations.append(decl)
435
+ if parent is not None:
436
+ parent.children.append(decl)
437
+ return
438
+ for child in node.named_children:
439
+ self.visit(child, parent, scope, prefix)
440
+
441
+
442
+ def _extract_declarations(spec: GrammarSpec, offsets: OffsetMap, root: Any, path: str) -> list[_Declaration]:
443
+ return _DeclarationExtractor(spec, offsets, root, path).extract()
444
+
445
+
446
+ def _coalesce_declarations(offsets: OffsetMap, declarations: list[_Declaration]) -> list[_Declaration]:
447
+ """Combine adjacent value declarations without crossing syntax or scope boundaries."""
448
+ def category(decl: _Declaration) -> str:
449
+ if decl.kind != "module_region":
450
+ return ""
451
+ ntype = decl.node.type
452
+ first = next((child for child in decl.node.children if child.type != "comment"), None)
453
+ if (ntype in {"alias_declaration", "type_definition", "type_spec", "type_alias"}
454
+ or (first is not None and _text(offsets, first.start_byte, first.end_byte) in {"typedef", "using"})):
455
+ return ""
456
+ if ntype in {"preproc_def", "preproc_function_def"}:
457
+ return "macro"
458
+ return ntype if ntype in {"declaration", "var_declaration", "const_declaration", "variable_declarator",
459
+ "field_declaration", "constant_declaration", "assignment"} else ""
460
+
461
+ def owner(decl: _Declaration) -> Any:
462
+ node = decl.node.parent
463
+ while node is not None and node.type in {
464
+ "lexical_declaration", "variable_declaration", "export_statement", "expression_statement"
465
+ }:
466
+ node = node.parent
467
+ return node
468
+
469
+ result: list[_Declaration] = []
470
+ for decl in sorted(declarations, key=lambda item: (item.start_byte, -item.end_byte)):
471
+ previous = result[-1] if result else None
472
+ if previous is not None and category(decl) and category(decl) == category(previous):
473
+ gap = _text(offsets, previous.end_byte, decl.start_byte)
474
+ gap = re.sub(r"/\*.*?\*/|//[^\n]*", "", gap, flags=re.S)
475
+ if decl.node.type == "assignment":
476
+ gap = re.sub(r"#[^\n]*|(?m:^=begin\b).*?^=end\b", "", gap, flags=re.S | re.M)
477
+ if (previous.parent_id == decl.parent_id and owner(previous) == owner(decl)
478
+ and decl.start_byte >= previous.end_byte and not gap.strip()):
479
+ previous.end_byte = decl.end_byte
480
+ previous.signature += ", " + decl.signature
481
+ continue
482
+ result.append(decl)
483
+ return result
484
+
485
+
486
+ def _is_type_definition(spec: GrammarSpec, node: Any) -> bool:
487
+ """True when a C/C++ type node actually defines a body.
488
+
489
+ ``struct Point p = {...}`` and ``class Foo;`` carry a type node without a
490
+ body and must not split the enclosing function or introduce a chunk.
491
+ Other grammars only emit type declaration nodes for real definitions.
492
+ """
493
+
494
+ if not spec.c_declaration_rules:
495
+ return True
496
+ return node.child_by_field_name("body") is not None
497
+
498
+
499
+ def _make_declaration(
500
+ offsets: OffsetMap,
501
+ node: Any,
502
+ path: str,
503
+ parent: _Declaration | None,
504
+ kind: str,
505
+ extend_start: int | None,
506
+ *,
507
+ name: str | None = None,
508
+ ) -> _Declaration:
509
+ start_byte = node.start_byte
510
+ if extend_start is not None:
511
+ start_byte = min(start_byte, extend_start)
512
+ end_byte = node.end_byte
513
+ name = name if name is not None else _node_name(offsets, node)
514
+ scope = parent.qualified_name if parent is not None else ""
515
+ qualified = f"{parent.qualified_name}.{name}" if parent is not None and name else (name or scope)
516
+ symbol_id = canonical_symbol_id(path, kind, qualified or name, start_byte)
517
+ ancestors = (parent.ancestors + (parent.qualified_name,)) if parent is not None else ()
518
+ body_start = _body_start(node)
519
+ if body_start < 0:
520
+ body_start = start_byte
521
+ signature = offsets.raw[start_byte:body_start].decode("utf-8", errors="replace").strip()
522
+ if len(signature) > _SIGNATURE_LIMIT:
523
+ signature = signature[:_SIGNATURE_LIMIT]
524
+ return _Declaration(
525
+ node=node,
526
+ kind=kind,
527
+ name=name or f"<{kind}>",
528
+ qualified_name=qualified or name or f"<{kind}>",
529
+ parent_id=parent.symbol_id if parent is not None else None,
530
+ symbol_id=symbol_id,
531
+ start_byte=start_byte,
532
+ end_byte=end_byte,
533
+ ancestors=ancestors,
534
+ signature=signature,
535
+ )
536
+
537
+