1use tree_sitter::Node;
4
5use super::{field_text, line, named_children, text, LangSpec, RawContract, Target};
6use crate::facts::{Args, Import, ImportItem, RefKind, TemplatePart, TypeSource, ValueExpr};
7use crate::model::SymbolKind;
8
9pub struct Python;
10
11impl LangSpec for Python {
12 fn receiver(&self) -> &'static str {
13 "self"
14 }
15
16 fn def(&self, node: Node, src: &[u8], in_class: bool) -> Option<(SymbolKind, String)> {
17 let kind = match node.kind() {
18 "function_definition" if in_class => SymbolKind::Method,
19 "function_definition" => SymbolKind::Function,
20 "class_definition" => SymbolKind::Class,
21 _ => return None,
22 };
23 Some((kind, field_text(node, "name", src)?.to_string()))
24 }
25
26 fn contract(&self, node: Node, src: &[u8]) -> Option<RawContract> {
27 self.contract_at(node, src)
28 }
29
30 fn value(&self, node: Node, src: &[u8]) -> Option<(String, ValueExpr)> {
31 self.value_at(node, src)
32 }
33
34 fn variable(&self, node: Node, src: &[u8]) -> Option<String> {
35 let left = (node.kind() == "assignment").then(|| node.child_by_field_name("left"))??;
36 (left.kind() == "identifier").then(|| text(left, src).to_string())
37 }
38
39 fn import(&self, node: Node, src: &[u8]) -> Option<Vec<Import>> {
40 match node.kind() {
41 "import_statement" => {
43 let mut cur = node.walk();
44 let imports = node
45 .children_by_field_name("name", &mut cur)
46 .filter_map(|n| module_item(n, src))
47 .map(|(module, alias)| Import {
48 module,
49 level: 0,
50 items: vec![ImportItem::Module { alias }],
51 line: line(node),
52 file: None,
53 })
54 .collect();
55 Some(imports)
56 }
57 "import_from_statement" => {
59 let m = node.child_by_field_name("module_name")?;
60 let (module, level) = if m.kind() == "relative_import" {
61 let mut level = 0;
62 let mut module = String::new();
63 for c in named_children(m) {
64 match c.kind() {
65 "import_prefix" => level = text(c, src).len() as u32,
66 "dotted_name" => module = text(c, src).to_string(),
67 _ => {}
68 }
69 }
70 (module, level)
71 } else {
72 (text(m, src).to_string(), 0)
73 };
74 let mut items = Vec::new();
75 let mut cur = node.walk();
76 for n in node.children_by_field_name("name", &mut cur) {
77 if let Some((name, alias)) = module_item(n, src) {
78 items.push(ImportItem::Name { name, alias });
79 }
80 }
81 if named_children(node)
82 .iter()
83 .any(|c| c.kind() == "wildcard_import")
84 {
85 items.push(ImportItem::All);
86 }
87 Some(vec![Import {
88 module,
89 level,
90 items,
91 line: line(node),
92 file: None,
93 }])
94 }
95 _ => None,
96 }
97 }
98
99 fn reference(&self, node: Node, src: &[u8]) -> Option<(RefKind, Vec<String>)> {
100 if node.kind() != "call" {
101 return None;
102 }
103 Some((
104 RefKind::Call,
105 path(node.child_by_field_name("function")?, src)?,
106 ))
107 }
108
109 fn assignment(&self, node: Node, src: &[u8]) -> Option<(Target, TypeSource)> {
110 if node.kind() != "assignment" {
111 return None;
112 }
113 let left = node.child_by_field_name("left")?;
114 let target = match left.kind() {
115 "identifier" => Target::Var(text(left, src).to_string()),
116 "attribute"
117 if left
118 .child_by_field_name("object")
119 .is_some_and(|o| text(o, src) == "self") =>
120 {
121 Target::Attr(field_text(left, "attribute", src)?.to_string())
122 }
123 _ => return None,
124 };
125 let source = if let Some(t) = node.child_by_field_name("type") {
126 TypeSource::Annotation(type_path(t, src)?)
127 } else {
128 let right = node.child_by_field_name("right")?;
129 match right.kind() {
130 "call" => TypeSource::Construct(path(right.child_by_field_name("function")?, src)?),
131 "identifier" => TypeSource::Alias {
132 name: text(right, src).to_string(),
133 scope: String::new(),
134 },
135 _ => return None,
136 }
137 };
138 Some((target, source))
139 }
140
141 fn bases(&self, def: Node, src: &[u8]) -> Vec<(RefKind, Vec<String>)> {
142 def.child_by_field_name("superclasses")
143 .map(|a| {
144 named_children(a)
145 .into_iter()
146 .filter_map(|b| path(b, src))
147 .map(|p| (RefKind::Inherits, p))
148 .collect()
149 })
150 .unwrap_or_default()
151 }
152
153 fn returns(&self, def: Node, src: &[u8]) -> Option<Vec<String>> {
154 type_path(def.child_by_field_name("return_type")?, src)
155 }
156
157 fn param_names(&self, def: Node, src: &[u8]) -> Vec<String> {
158 let Some(ps) = def.child_by_field_name("parameters") else {
159 return Vec::new();
160 };
161 let mut names: Vec<String> = named_children(ps)
162 .into_iter()
163 .filter_map(|p| match p.kind() {
164 "identifier" => Some(text(p, src).to_string()),
165 "list_splat_pattern" | "keyword_separator" => Some("*".to_string()),
167 "typed_parameter" | "default_parameter" | "typed_default_parameter" => p
168 .child_by_field_name("name")
169 .or_else(|| {
170 named_children(p)
171 .into_iter()
172 .find(|c| c.kind() == "identifier")
173 })
174 .map(|n| text(n, src).to_string()),
175 _ => None,
176 })
177 .collect();
178 if names.first().is_some_and(|n| n == "self" || n == "cls") {
179 names.remove(0);
180 }
181 names
182 }
183
184 fn call(&self, node: Node, src: &[u8]) -> Option<(Vec<String>, Args)> {
185 if node.kind() != "call" {
186 return None;
187 }
188 let callee = path(node.child_by_field_name("function")?, src)?;
189 let a = args(node.child_by_field_name("arguments")?, src);
190 (a.positional.iter().any(Option::is_some) || !a.keywords.is_empty()).then_some((callee, a))
191 }
192
193 fn params(&self, def: Node, src: &[u8]) -> Vec<(String, Vec<String>, bool)> {
194 let Some(ps) = def.child_by_field_name("parameters") else {
195 return Vec::new();
196 };
197 named_children(ps)
198 .into_iter()
199 .filter(|p| matches!(p.kind(), "typed_parameter" | "typed_default_parameter"))
200 .filter_map(|p| {
201 let name = p.child_by_field_name("name").or_else(|| {
202 named_children(p)
203 .into_iter()
204 .find(|c| c.kind() == "identifier")
205 })?;
206 let ty = type_path(p.child_by_field_name("type")?, src)?;
207 Some((text(name, src).to_string(), ty, false))
208 })
209 .collect()
210 }
211}
212
213fn module_item(n: Node, src: &[u8]) -> Option<(String, Option<String>)> {
215 match n.kind() {
216 "aliased_import" => Some((
217 field_text(n, "name", src)?.to_string(),
218 field_text(n, "alias", src).map(str::to_string),
219 )),
220 "dotted_name" => Some((text(n, src).to_string(), None)),
221 _ => None,
222 }
223}
224
225fn path(node: Node, src: &[u8]) -> Option<Vec<String>> {
227 match node.kind() {
228 "identifier" => Some(vec![text(node, src).to_string()]),
229 "attribute" => {
230 let mut p = path(node.child_by_field_name("object")?, src)?;
231 p.push(field_text(node, "attribute", src)?.to_string());
232 Some(p)
233 }
234 _ => None,
235 }
236}
237
238fn type_path(node: Node, src: &[u8]) -> Option<Vec<String>> {
240 match node.kind() {
241 "type" => type_path(named_children(node).into_iter().next()?, src),
242 "generic_type" | "subscript" => {
243 let head = named_children(node).into_iter().next()?;
244 let name = text(head, src);
245 if matches!(name, "Optional" | "Annotated" | "typing.Optional") {
246 let arg = named_children(node).into_iter().nth(1)?;
247 type_path(arg, src)
248 } else {
249 type_path(head, src)
250 }
251 }
252 "type_parameter" => type_path(named_children(node).into_iter().next()?, src),
253 "binary_operator" => type_path(node.child_by_field_name("left")?, src),
254 "string" => {
256 let t = text(node, src).trim_matches(|c| c == '"' || c == '\'');
257 (!t.is_empty()
258 && t.chars()
259 .all(|c| c.is_alphanumeric() || c == '_' || c == '.'))
260 .then(|| t.split('.').map(str::to_string).collect())
261 }
262 _ => path(node, src),
263 }
264}
265
266const ROUTE_METHODS: &[&str] = &["get", "post", "put", "patch", "delete", "head", "options"];
269
270const CTORS: &[&str] = &[
272 "APIRouter",
273 "FastAPI",
274 "Client",
275 "AsyncClient",
276 "Session",
277 "ClientSession",
278 "client",
279 "resource",
280];
281
282const OPS: &[&str] = &[
284 "get",
285 "post",
286 "put",
287 "patch",
288 "delete",
289 "head",
290 "options",
291 "request",
292 "stream",
293 "send_message",
294 "send_message_batch",
295 "receive_message",
296 "publish",
297 "publish_batch",
298 "put_object",
299 "get_object",
300 "upload_file",
301 "upload_fileobj",
302 "download_file",
303 "download_fileobj",
304 "head_object",
305 "delete_object",
306 "list_objects_v2",
307 "copy_object",
308 "include_router",
309 "from_",
310 "into",
311 "join",
312 "query",
313 "add",
314 "merge",
315 "execute",
316 "select",
317 "insert",
318 "update",
319 "text",
320];
321
322impl Python {
323 pub(crate) fn contract_at(&self, node: Node, src: &[u8]) -> Option<RawContract> {
324 match node.kind() {
325 "decorated_definition" => {
326 let def = node.child_by_field_name("definition")?;
327 let handler = field_text(def, "name", src)?.to_string();
328 named_children(node)
329 .into_iter()
330 .filter(|d| d.kind() == "decorator")
331 .find_map(|d| {
332 let call = named_children(d).into_iter().find(|c| c.kind() == "call")?;
333 let f = call.child_by_field_name("function")?;
334 if f.kind() != "attribute" {
335 return None;
336 }
337 let method = field_text(f, "attribute", src)?;
338 if !ROUTE_METHODS.contains(&method) {
339 return None;
340 }
341 let args = args(call.child_by_field_name("arguments")?, src);
342 Some(RawContract::Route {
343 handler: handler.clone(),
344 receiver: path(f.child_by_field_name("object")?, src)?,
345 method: method.to_uppercase(),
346 path: args.get("path", 0)?.clone(),
347 client: false,
348 })
349 })
350 }
351 "assignment" => {
352 let left = node.child_by_field_name("left")?;
353 let right = node.child_by_field_name("right")?;
354 if left.kind() == "identifier" && text(left, src) == "__tablename__" {
355 if let Some(ValueExpr::Lit(t)) = value_expr(right, src) {
356 return Some(RawContract::TableName(t));
357 }
358 }
359 if right.kind() != "call" {
360 return None;
361 }
362 let ctor = path(right.child_by_field_name("function")?, src)?;
363 let name = ctor.last()?.as_str();
364 if !CTORS.contains(&name) && !name.ends_with("Router") {
366 return None;
367 }
368 let (target, _) = self.assignment(node, src)?;
369 Some(RawContract::Construct {
370 target,
371 ctor,
372 args: args(right.child_by_field_name("arguments")?, src),
373 })
374 }
375 "call" => {
376 let f = node.child_by_field_name("function")?;
377 let (receiver, method) = match f.kind() {
378 "identifier" => (Vec::new(), text(f, src).to_string()),
379 "attribute" => (
380 path(f.child_by_field_name("object")?, src).unwrap_or_default(),
381 field_text(f, "attribute", src)?.to_string(),
382 ),
383 _ => return None,
384 };
385 if !OPS.contains(&method.as_str()) {
386 return None;
387 }
388 Some(RawContract::Op {
389 receiver,
390 method,
391 args: args(node.child_by_field_name("arguments")?, src),
392 })
393 }
394 _ => None,
395 }
396 }
397
398 pub(crate) fn value_at(&self, node: Node, src: &[u8]) -> Option<(String, ValueExpr)> {
400 if node.kind() != "assignment" {
401 return None;
402 }
403 let left = node.child_by_field_name("left")?;
404 let name = match left.kind() {
405 "identifier" => text(left, src).to_string(),
406 "attribute"
407 if left
408 .child_by_field_name("object")
409 .is_some_and(|o| text(o, src) == "self") =>
410 {
411 format!("self.{}", field_text(left, "attribute", src)?)
412 }
413 _ => return None,
414 };
415 let v = value_expr(node.child_by_field_name("right")?, src)?;
416 Some((name, v))
417 }
418}
419
420fn args(list: Node, src: &[u8]) -> Args {
421 let mut out = Args::default();
422 for a in named_children(list) {
423 match a.kind() {
424 "keyword_argument" => {
425 if let (Some(k), Some(v)) = (
426 field_text(a, "name", src),
427 a.child_by_field_name("value")
428 .and_then(|v| value_expr(v, src)),
429 ) {
430 out.keywords.push((k.to_string(), v));
431 }
432 }
433 "dictionary_splat" => {
435 if let Some(v) = named_children(a).first().and_then(|v| value_expr(*v, src)) {
436 out.keywords.push(("**".into(), v));
437 }
438 }
439 "list_splat" | "comment" => {}
440 _ => {
441 if a.kind() == "call" {
442 if let Some(p) = a.child_by_field_name("function").and_then(|f| path(f, src)) {
443 out.calls.push((out.positional.len(), p));
444 }
445 }
446 out.positional.push(value_expr(a, src));
447 }
448 }
449 }
450 out
451}
452
453fn value_expr(node: Node, src: &[u8]) -> Option<ValueExpr> {
455 match node.kind() {
456 "conditional_expression" => value_expr(*named_children(node).first()?, src),
458 "await" => value_expr(*named_children(node).first()?, src),
459 "string" => {
460 let mut parts = Vec::new();
461 for c in named_children(node) {
462 match c.kind() {
463 "string_content" => parts.push(TemplatePart::Lit(text(c, src).to_string())),
464 "interpolation" => parts.push(TemplatePart::Expr(
465 c.child_by_field_name("expression")
466 .and_then(|e| path(e, src)),
467 )),
468 _ => {}
469 }
470 }
471 if parts.iter().all(|p| matches!(p, TemplatePart::Lit(_))) {
472 let s: String = parts
473 .into_iter()
474 .map(|p| match p {
475 TemplatePart::Lit(s) => s,
476 TemplatePart::Expr(_) => String::new(),
477 })
478 .collect();
479 Some(ValueExpr::Lit(s))
480 } else {
481 Some(ValueExpr::Template(parts))
482 }
483 }
484 "concatenated_string" => {
485 let mut parts = Vec::new();
486 for c in named_children(node) {
487 match value_expr(c, src)? {
488 ValueExpr::Lit(s) => parts.push(TemplatePart::Lit(s)),
489 ValueExpr::Template(p) => parts.extend(p),
490 _ => return None,
491 }
492 }
493 Some(ValueExpr::Template(parts))
494 }
495 "subscript" => {
497 let value = node.child_by_field_name("value")?;
498 if path(value, src)?.join(".") != "os.environ" {
499 return None;
500 }
501 match value_expr(node.child_by_field_name("subscript")?, src)? {
502 ValueExpr::Lit(k) => Some(ValueExpr::Env(k)),
503 _ => None,
504 }
505 }
506 "call" => {
508 let f = path(node.child_by_field_name("function")?, src)?.join(".");
509 let list = named_children(node.child_by_field_name("arguments")?);
511 let attr = match (f.as_str(), list.as_slice()) {
512 ("getattr", [obj, name, ..]) => Some((*obj, *name)),
513 (t, [g, obj, name, ..])
514 if t.ends_with("to_thread") && text(*g, src) == "getattr" =>
515 {
516 Some((*obj, *name))
517 }
518 _ => None,
519 };
520 if let Some((obj, name)) = attr {
521 let (Some(mut p), Some(ValueExpr::Lit(n))) =
522 (path(obj, src), value_expr(name, src))
523 else {
524 return None;
525 };
526 p.push(n);
527 return Some(ValueExpr::Name(p));
528 }
529 if !matches!(
530 f.as_str(),
531 "os.getenv" | "getenv" | "os.environ.get" | "environ.get"
532 ) {
533 return call_value(node, src);
534 }
535 let first = named_children(node.child_by_field_name("arguments")?)
536 .into_iter()
537 .next()?;
538 match value_expr(first, src)? {
539 ValueExpr::Lit(k) => Some(ValueExpr::Env(k)),
540 _ => None,
541 }
542 }
543 "identifier" | "attribute" => path(node, src).map(ValueExpr::Name),
544 "binary_operator" => {
545 let sep = match field_text(node, "operator", src)? {
546 "/" => "/",
547 "+" => "",
548 _ => return None,
549 };
550 let mut parts = Vec::new();
551 for side in ["left", "right"] {
552 match value_expr(node.child_by_field_name(side)?, src)? {
553 ValueExpr::Join { sep: s, parts: p } if s == sep => parts.extend(p),
554 v => parts.push(v),
555 }
556 }
557 Some(ValueExpr::Join {
558 sep: sep.to_string(),
559 parts,
560 })
561 }
562 "dictionary" => Some(ValueExpr::Record(
563 named_children(node)
564 .into_iter()
565 .filter(|p| p.kind() == "pair")
566 .filter_map(|p| {
567 let ValueExpr::Lit(k) = value_expr(p.child_by_field_name("key")?, src)? else {
568 return None;
569 };
570 Some((k, value_expr(p.child_by_field_name("value")?, src)?))
571 })
572 .collect(),
573 )),
574 "parenthesized_expression" => value_expr(named_children(node).into_iter().next()?, src),
575 _ => None,
576 }
577}
578
579fn call_value(node: Node, src: &[u8]) -> Option<ValueExpr> {
581 let func = path(node.child_by_field_name("function")?, src)?;
582 let a = args(node.child_by_field_name("arguments")?, src);
583 Some(ValueExpr::Call {
584 func,
585 args: a.positional.into_iter().map_while(|v| v).collect(),
586 kwargs: a.keywords,
587 })
588}