brokk_bifrost_python/
syntax.rs1use brokk_bifrost_core::analyzer::Range;
2use brokk_bifrost_core::cancellation::CancellationToken;
3use brokk_bifrost_core::hash::HashSet;
4use tree_sitter::{Node, Tree};
5
6pub fn python_static_attribute_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
12 if !matches!(node.kind(), "identifier" | "attribute") {
13 return None;
14 }
15 let mut path = Vec::new();
16 loop {
17 match node.kind() {
18 "identifier" => {
19 path.push(node);
20 break;
21 }
22 "attribute" => {
23 let attribute = node.child_by_field_name("attribute")?;
24 if attribute.kind() != "identifier" {
25 return None;
26 }
27 path.push(attribute);
28 node = node.child_by_field_name("object")?;
29 }
30 _ => return None,
31 }
32 }
33 path.reverse();
34 Some(path)
35}
36
37pub fn python_static_type_path<'tree>(mut node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
41 let mut path = Vec::new();
42 loop {
43 match node.kind() {
44 "identifier" => {
45 path.push(node);
46 break;
47 }
48 "type" | "generic_type" | "subscript" => node = node.named_child(0)?,
49 "attribute" => {
50 let attribute = node.child_by_field_name("attribute")?;
51 if attribute.kind() != "identifier" {
52 return None;
53 }
54 path.push(attribute);
55 node = node.child_by_field_name("object")?;
56 }
57 "member_type" => {
58 let mut cursor = node.walk();
59 let mut children = node.named_children(&mut cursor);
60 let qualifier = children.next()?;
61 let member = children.next()?;
62 if member.kind() != "identifier" || children.next().is_some() {
63 return None;
64 }
65 path.push(member);
66 node = qualifier;
67 }
68 _ => return None,
69 }
70 }
71 path.reverse();
72 Some(path)
73}
74
75pub fn python_plain_string_literal<'source>(
81 node: Node<'_>,
82 source: &'source str,
83) -> Option<&'source str> {
84 if node.kind() != "string"
85 || node
86 .parent()
87 .is_some_and(|parent| parent.kind() == "concatenated_string")
88 {
89 return None;
90 }
91 let mut content = None;
92 let mut cursor = node.walk();
93 for child in node.named_children(&mut cursor) {
94 match child.kind() {
95 "string_start" | "string_end" => {
96 let delimiter = child.utf8_text(source.as_bytes()).ok()?;
97 if delimiter
98 .chars()
99 .any(|character| character != '"' && character != '\'')
100 {
101 return None;
102 }
103 }
104 "string_content" if child.named_child_count() == 0 && content.is_none() => {
105 content = Some(child);
106 }
107 _ => return None,
108 }
109 }
110 Some(content.map_or("", |child| {
111 child
112 .utf8_text(source.as_bytes())
113 .expect("a tree-sitter node range is valid UTF-8 source")
114 }))
115}
116
117#[derive(Debug, Default)]
118pub struct PythonOverloadDecoratorBindings {
119 direct: HashSet<String>,
120 namespaces: HashSet<String>,
121}
122
123impl PythonOverloadDecoratorBindings {
124 pub fn collect(root: Node<'_>, source: &str) -> Self {
125 let mut bindings = Self::default();
126 let mut stack = vec![root];
127
128 while let Some(node) = stack.pop() {
129 match node.kind() {
130 "function_definition" | "class_definition" | "lambda" => continue,
131 "import_statement" => bindings.collect_namespace_imports(node, source),
132 "import_from_statement" => bindings.collect_direct_imports(node, source),
133 _ => {}
134 }
135
136 let mut cursor = node.walk();
137 let children: Vec<_> = node.named_children(&mut cursor).collect();
138 stack.extend(children.into_iter().rev());
139 }
140
141 bindings
142 }
143
144 fn collect_namespace_imports(&mut self, node: Node<'_>, source: &str) {
145 let mut cursor = node.walk();
146 for imported in node.children_by_field_name("name", &mut cursor) {
147 match imported.kind() {
148 "dotted_name" => {
149 let module = node_text(imported, source).trim();
150 if is_typing_module(module) {
151 self.namespaces.insert(module.to_string());
152 }
153 }
154 "aliased_import" => {
155 let Some(name) = imported.child_by_field_name("name") else {
156 continue;
157 };
158 if !is_typing_module(node_text(name, source).trim()) {
159 continue;
160 }
161 let Some(alias) = imported.child_by_field_name("alias") else {
162 continue;
163 };
164 let alias = node_text(alias, source).trim();
165 if !alias.is_empty() {
166 self.namespaces.insert(alias.to_string());
167 }
168 }
169 _ => {}
170 }
171 }
172 }
173
174 fn collect_direct_imports(&mut self, node: Node<'_>, source: &str) {
175 let Some(module) = node.child_by_field_name("module_name") else {
176 return;
177 };
178 if !is_typing_module(node_text(module, source).trim()) {
179 return;
180 }
181
182 let mut cursor = node.walk();
183 for imported in node.children_by_field_name("name", &mut cursor) {
184 match imported.kind() {
185 "dotted_name" if node_text(imported, source).trim() == "overload" => {
186 self.direct.insert("overload".to_string());
187 }
188 "aliased_import" => {
189 let Some(name) = imported.child_by_field_name("name") else {
190 continue;
191 };
192 if node_text(name, source).trim() != "overload" {
193 continue;
194 }
195 let Some(alias) = imported.child_by_field_name("alias") else {
196 continue;
197 };
198 let alias = node_text(alias, source).trim();
199 if !alias.is_empty() {
200 self.direct.insert(alias.to_string());
201 }
202 }
203 _ => {}
204 }
205 }
206 }
207
208 pub fn decorates_as_overload(&self, function: Node<'_>, source: &str) -> bool {
209 let Some(parent) = function
210 .parent()
211 .filter(|node| node.kind() == "decorated_definition")
212 else {
213 return false;
214 };
215
216 let mut cursor = parent.walk();
217 parent
218 .named_children(&mut cursor)
219 .filter(|child| child.kind() == "decorator")
220 .filter_map(decorator_callee)
221 .any(|callee| match callee.kind() {
222 "identifier" => self.direct.contains(node_text(callee, source).trim()),
223 "attribute" => {
224 let Some(attribute) = callee.child_by_field_name("attribute") else {
225 return false;
226 };
227 if node_text(attribute, source).trim() != "overload" {
228 return false;
229 }
230 let Some(object) = callee.child_by_field_name("object") else {
231 return false;
232 };
233 object.kind() == "identifier"
234 && self.namespaces.contains(node_text(object, source).trim())
235 }
236 _ => false,
237 })
238 }
239}
240
241fn is_typing_module(module: &str) -> bool {
242 matches!(module, "typing" | "typing_extensions")
243}
244
245fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
246 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
247}
248
249pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
251 let mut current = expression;
252 loop {
253 match current.kind() {
254 "identifier" => return Some(current),
255 "attribute" => current = current.child_by_field_name("attribute")?,
256 "call" => current = current.child_by_field_name("function")?,
257 _ => return None,
258 }
259 }
260}
261
262pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
270 node.kind() == "identifier"
271 && node.parent().is_some_and(|parent| {
272 parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
273 })
274}
275
276pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
278 if decorator.kind() != "decorator" {
279 return None;
280 }
281 let mut expression = decorator.named_child(0)?;
282 while expression.kind() == "call" {
283 expression = expression.child_by_field_name("function")?;
284 }
285 Some(expression)
286}
287
288pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
291 let start = node.start_byte();
292 let end = node.end_byte();
293 let mut current = node;
294 while let Some(parent) = current.parent() {
295 let annotation = match parent.kind() {
296 "function_definition" => parent.child_by_field_name("return_type"),
297 "typed_parameter" | "typed_default_parameter" | "assignment" => {
298 parent.child_by_field_name("type")
299 }
300 _ => None,
301 };
302 if let Some(annotation) = annotation
303 && annotation.start_byte() <= start
304 && end <= annotation.end_byte()
305 {
306 return true;
307 }
308 current = parent;
309 }
310 false
311}
312
313pub fn python_deferred_annotation_identifier_ranges(
315 string: Node<'_>,
316 source: &str,
317 cancellation: Option<&CancellationToken>,
318) -> Option<Vec<Range>> {
319 let tree = python_deferred_annotation_tree(string, source, cancellation)?;
320
321 let mut ranges = Vec::new();
322 let mut stack = vec![tree.root_node()];
323 while let Some(current) = stack.pop() {
324 if cancellation.is_some_and(CancellationToken::is_cancelled) {
325 return None;
326 }
327 if current.kind() == "identifier" {
328 ranges.push(Range {
329 start_byte: current.start_byte(),
330 end_byte: current.end_byte(),
331 start_line: current.start_position().row + 1,
332 end_line: current.end_position().row + 1,
333 });
334 }
335 for index in (0..current.named_child_count()).rev() {
336 if let Some(child) = current.named_child(index) {
337 stack.push(child);
338 }
339 }
340 }
341 Some(ranges)
342}
343
344pub fn python_deferred_annotation_tree(
348 string: Node<'_>,
349 source: &str,
350 cancellation: Option<&CancellationToken>,
351) -> Option<Tree> {
352 if string.kind() != "string"
353 || string
354 .parent()
355 .is_some_and(|parent| parent.kind() == "concatenated_string")
356 || !python_node_is_in_annotation(string)
357 || python_string_is_literal_value(string, source)
358 {
359 return None;
360 }
361
362 let mut content = None;
363 for index in 0..string.named_child_count() {
364 let child = string.named_child(index)?;
365 match child.kind() {
366 "string_start" | "string_end" => {}
367 "string_content" if content.is_none() => content = Some(child),
368 _ => return None,
369 }
370 }
371 let content = content?;
372 let language = tree_sitter_python::LANGUAGE.into();
373 let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
374 &language,
375 source,
376 content.range(),
377 cancellation,
378 )?;
379 if tree.root_node().has_error() {
380 return None;
381 }
382 Some(tree)
383}
384
385fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
389 let start = string.start_byte();
390 let end = string.end_byte();
391 let mut current = string;
392 while let Some(parent) = current.parent() {
393 match parent.kind() {
394 "subscript" => {
395 let Some(value) = parent.child_by_field_name("value") else {
396 return false;
397 };
398 if value.start_byte() <= start && end <= value.end_byte() {
399 return false;
400 }
401 return python_literal_annotation_base(value, source);
402 }
403 "generic_type" => {
404 let Some(value) = parent.named_child(0) else {
405 return false;
406 };
407 return python_literal_annotation_base(value, source);
408 }
409 _ => current = parent,
410 }
411 }
412 false
413}
414
415fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
416 match value.kind() {
417 "identifier" => node_text(value, source) == "Literal",
418 "attribute" => {
419 let (Some(object), Some(attribute)) = (
420 value.child_by_field_name("object"),
421 value.child_by_field_name("attribute"),
422 ) else {
423 return false;
424 };
425 object.kind() == "identifier"
426 && matches!(node_text(object, source), "typing" | "typing_extensions")
427 && attribute.kind() == "identifier"
428 && node_text(attribute, source) == "Literal"
429 }
430 "member_type" => {
431 let mut identifiers = Vec::new();
432 let mut stack = vec![value];
433 while let Some(node) = stack.pop() {
434 if node.kind() == "identifier" {
435 identifiers.push(node_text(node, source));
436 continue;
437 }
438 for index in (0..node.named_child_count()).rev() {
439 if let Some(child) = node.named_child(index) {
440 stack.push(child);
441 }
442 }
443 }
444 matches!(
445 identifiers.as_slice(),
446 ["typing", "Literal"] | ["typing_extensions", "Literal"]
447 )
448 }
449 _ => false,
450 }
451}