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
123#[derive(Debug)]
124pub(crate) enum PythonOverloadDecoratorName {
125 Direct(String),
126 Namespace(String),
127}
128
129impl PythonOverloadDecoratorBindings {
130 pub(crate) fn collect_import(
133 &mut self,
134 import: &brokk_bifrost_core::analyzer::model::ImportInfo,
135 ) {
136 use brokk_bifrost_core::analyzer::model::StructuredImportPathKind;
137 let Some(path) = &import.path else { return };
138 match (path.kind, path.segments.as_slice()) {
139 (Some(StructuredImportPathKind::Namespace), [module]) if is_typing_module(module) => {
140 self.namespaces
141 .insert(import.alias.as_ref().unwrap_or(module).clone());
142 }
143 (Some(StructuredImportPathKind::ImportFrom), [module, name])
144 if is_typing_module(module) && name == "overload" =>
145 {
146 self.direct
147 .insert(import.alias.as_ref().unwrap_or(name).clone());
148 }
149 _ => {}
150 }
151 }
152
153 pub(crate) fn overload_decorator_names(
158 function: Node<'_>,
159 source: &str,
160 ) -> Vec<PythonOverloadDecoratorName> {
161 let Some(parent) = function
162 .parent()
163 .filter(|node| node.kind() == "decorated_definition")
164 else {
165 return Vec::new();
166 };
167 let mut cursor = parent.walk();
168 parent
169 .named_children(&mut cursor)
170 .filter(|child| child.kind() == "decorator")
171 .filter_map(decorator_callee)
172 .filter_map(|callee| match callee.kind() {
173 "identifier" => Some(PythonOverloadDecoratorName::Direct(
174 node_text(callee, source).trim().to_string(),
175 )),
176 "attribute" => {
177 let attribute = callee.child_by_field_name("attribute")?;
178 if node_text(attribute, source).trim() != "overload" {
179 return None;
180 }
181 let object = callee.child_by_field_name("object")?;
182 (object.kind() == "identifier").then(|| {
183 PythonOverloadDecoratorName::Namespace(
184 node_text(object, source).trim().to_string(),
185 )
186 })
187 }
188 _ => None,
189 })
190 .collect()
191 }
192
193 pub(crate) fn matches_overload_binding(&self, name: &PythonOverloadDecoratorName) -> bool {
194 match name {
195 PythonOverloadDecoratorName::Direct(name) => self.direct.contains(name),
196 PythonOverloadDecoratorName::Namespace(name) => self.namespaces.contains(name),
197 }
198 }
199}
200
201fn is_typing_module(module: &str) -> bool {
202 matches!(module, "typing" | "typing_extensions")
203}
204
205fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
206 brokk_bifrost_core::analyzer::common::node_source_text(node, source)
207}
208
209pub fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
211 let mut current = expression;
212 loop {
213 match current.kind() {
214 "identifier" => return Some(current),
215 "attribute" => current = current.child_by_field_name("attribute")?,
216 "call" => current = current.child_by_field_name("function")?,
217 _ => return None,
218 }
219 }
220}
221
222pub fn python_keyword_argument_label(node: Node<'_>) -> bool {
230 node.kind() == "identifier"
231 && node.parent().is_some_and(|parent| {
232 parent.kind() == "keyword_argument" && parent.child_by_field_name("name") == Some(node)
233 })
234}
235
236pub fn decorator_callee<'tree>(decorator: Node<'tree>) -> Option<Node<'tree>> {
238 if decorator.kind() != "decorator" {
239 return None;
240 }
241 let mut expression = decorator.named_child(0)?;
242 while expression.kind() == "call" {
243 expression = expression.child_by_field_name("function")?;
244 }
245 Some(expression)
246}
247
248pub fn python_node_is_in_annotation(node: Node<'_>) -> bool {
251 let start = node.start_byte();
252 let end = node.end_byte();
253 let mut current = node;
254 while let Some(parent) = current.parent() {
255 let annotation = match parent.kind() {
256 "function_definition" => parent.child_by_field_name("return_type"),
257 "typed_parameter" | "typed_default_parameter" | "assignment" => {
258 parent.child_by_field_name("type")
259 }
260 _ => None,
261 };
262 if let Some(annotation) = annotation
263 && annotation.start_byte() <= start
264 && end <= annotation.end_byte()
265 {
266 return true;
267 }
268 current = parent;
269 }
270 false
271}
272
273pub fn python_deferred_annotation_identifier_ranges(
275 string: Node<'_>,
276 source: &str,
277 cancellation: Option<&CancellationToken>,
278) -> Option<Vec<Range>> {
279 let tree = python_deferred_annotation_tree(string, source, cancellation)?;
280
281 let mut ranges = Vec::new();
282 let mut stack = vec![tree.root_node()];
283 while let Some(current) = stack.pop() {
284 if cancellation.is_some_and(CancellationToken::is_cancelled) {
285 return None;
286 }
287 if current.kind() == "identifier" {
288 ranges.push(Range {
289 start_byte: current.start_byte(),
290 end_byte: current.end_byte(),
291 start_line: current.start_position().row + 1,
292 end_line: current.end_position().row + 1,
293 });
294 }
295 for index in (0..current.named_child_count()).rev() {
296 if let Some(child) = current.named_child(index) {
297 stack.push(child);
298 }
299 }
300 }
301 Some(ranges)
302}
303
304pub fn python_deferred_annotation_tree(
308 string: Node<'_>,
309 source: &str,
310 cancellation: Option<&CancellationToken>,
311) -> Option<Tree> {
312 if string.kind() != "string"
313 || string
314 .parent()
315 .is_some_and(|parent| parent.kind() == "concatenated_string")
316 || !python_node_is_in_annotation(string)
317 || python_string_is_literal_value(string, source)
318 {
319 return None;
320 }
321
322 let mut content = None;
323 for index in 0..string.named_child_count() {
324 let child = string.named_child(index)?;
325 match child.kind() {
326 "string_start" | "string_end" => {}
327 "string_content" if content.is_none() => content = Some(child),
328 _ => return None,
329 }
330 }
331 let content = content?;
332 let language = tree_sitter_python::LANGUAGE.into();
333 let tree = brokk_bifrost_core::analyzer::common::parse_source_range_with_cancellation(
334 &language,
335 source,
336 content.range(),
337 cancellation,
338 )?;
339 if tree.root_node().has_error() {
340 return None;
341 }
342 Some(tree)
343}
344
345fn python_string_is_literal_value(string: Node<'_>, source: &str) -> bool {
349 let start = string.start_byte();
350 let end = string.end_byte();
351 let mut current = string;
352 while let Some(parent) = current.parent() {
353 match parent.kind() {
354 "subscript" => {
355 let Some(value) = parent.child_by_field_name("value") else {
356 return false;
357 };
358 if value.start_byte() <= start && end <= value.end_byte() {
359 return false;
360 }
361 return python_literal_annotation_base(value, source);
362 }
363 "generic_type" => {
364 let Some(value) = parent.named_child(0) else {
365 return false;
366 };
367 return python_literal_annotation_base(value, source);
368 }
369 _ => current = parent,
370 }
371 }
372 false
373}
374
375fn python_literal_annotation_base(value: Node<'_>, source: &str) -> bool {
376 match value.kind() {
377 "identifier" => node_text(value, source) == "Literal",
378 "attribute" => {
379 let (Some(object), Some(attribute)) = (
380 value.child_by_field_name("object"),
381 value.child_by_field_name("attribute"),
382 ) else {
383 return false;
384 };
385 object.kind() == "identifier"
386 && matches!(node_text(object, source), "typing" | "typing_extensions")
387 && attribute.kind() == "identifier"
388 && node_text(attribute, source) == "Literal"
389 }
390 "member_type" => {
391 let mut identifiers = Vec::new();
392 let mut stack = vec![value];
393 while let Some(node) = stack.pop() {
394 if node.kind() == "identifier" {
395 identifiers.push(node_text(node, source));
396 continue;
397 }
398 for index in (0..node.named_child_count()).rev() {
399 if let Some(child) = node.named_child(index) {
400 stack.push(child);
401 }
402 }
403 }
404 matches!(
405 identifiers.as_slice(),
406 ["typing", "Literal"] | ["typing_extensions", "Literal"]
407 )
408 }
409 _ => false,
410 }
411}