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.
- cbe/__init__.py +9 -0
- cbe/__main__.py +4 -0
- cbe/aggregation.py +183 -0
- cbe/api_completion.py +410 -0
- cbe/batching.py +283 -0
- cbe/catalog_query.py +376 -0
- cbe/checks.py +390 -0
- cbe/cli.py +434 -0
- cbe/concrete_facts.py +321 -0
- cbe/data/LICENSE.tiktoken +30 -0
- cbe/data/o200k_base.tiktoken +199998 -0
- cbe/declarations.py +537 -0
- cbe/docs.py +929 -0
- cbe/generate.py +316 -0
- cbe/grammars.py +741 -0
- cbe/graph.py +857 -0
- cbe/graph_backends/__init__.py +43 -0
- cbe/graph_backends/_calls.py +262 -0
- cbe/graph_backends/_paths.py +42 -0
- cbe/graph_backends/identity.py +93 -0
- cbe/graph_backends/imports.py +256 -0
- cbe/graph_backends/joern.py +1021 -0
- cbe/graph_backends/resolution.py +288 -0
- cbe/graph_backends/toolchain.py +396 -0
- cbe/grounding.py +57 -0
- cbe/host_run.py +1752 -0
- cbe/model_settings.py +80 -0
- cbe/projection.py +305 -0
- cbe/prompts.py +316 -0
- cbe/records.py +200 -0
- cbe/skeleton.py +474 -0
- cbe/source_context.py +709 -0
- cbe/symbols.py +136 -0
- cbe/token_budget.py +68 -0
- cbe/treename.py +398 -0
- cbe/usage.py +300 -0
- cbe_install/__init__.py +11 -0
- cbe_install/cli.py +434 -0
- cbe_install/resources/agents/cbe-writer.md +16 -0
- cbe_install/resources/codebase-explorer/SKILL.md +101 -0
- cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
- cbe_install/resources/codebase-explorer/references/codex.md +37 -0
- codebase_explorer-2.2.0.dist-info/METADATA +233 -0
- codebase_explorer-2.2.0.dist-info/RECORD +47 -0
- codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
- codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
- 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
|
+
|