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