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 if !CTORS.contains(&ctor.last()?.as_str()) {
364 return None;
365 }
366 let (target, _) = self.assignment(node, src)?;
367 Some(RawContract::Construct {
368 target,
369 ctor,
370 args: args(right.child_by_field_name("arguments")?, src),
371 })
372 }
373 "call" => {
374 let f = node.child_by_field_name("function")?;
375 let (receiver, method) = match f.kind() {
376 "identifier" => (Vec::new(), text(f, src).to_string()),
377 "attribute" => (
378 path(f.child_by_field_name("object")?, src).unwrap_or_default(),
379 field_text(f, "attribute", src)?.to_string(),
380 ),
381 _ => return None,
382 };
383 if !OPS.contains(&method.as_str()) {
384 return None;
385 }
386 Some(RawContract::Op {
387 receiver,
388 method,
389 args: args(node.child_by_field_name("arguments")?, src),
390 })
391 }
392 _ => None,
393 }
394 }
395
396 pub(crate) fn value_at(&self, node: Node, src: &[u8]) -> Option<(String, ValueExpr)> {
398 if node.kind() != "assignment" {
399 return None;
400 }
401 let left = node.child_by_field_name("left")?;
402 let name = match left.kind() {
403 "identifier" => text(left, src).to_string(),
404 "attribute"
405 if left
406 .child_by_field_name("object")
407 .is_some_and(|o| text(o, src) == "self") =>
408 {
409 format!("self.{}", field_text(left, "attribute", src)?)
410 }
411 _ => return None,
412 };
413 let v = value_expr(node.child_by_field_name("right")?, src)?;
414 Some((name, v))
415 }
416}
417
418fn args(list: Node, src: &[u8]) -> Args {
419 let mut out = Args::default();
420 for a in named_children(list) {
421 match a.kind() {
422 "keyword_argument" => {
423 if let (Some(k), Some(v)) = (
424 field_text(a, "name", src),
425 a.child_by_field_name("value")
426 .and_then(|v| value_expr(v, src)),
427 ) {
428 out.keywords.push((k.to_string(), v));
429 }
430 }
431 "dictionary_splat" => {
433 if let Some(v) = named_children(a).first().and_then(|v| value_expr(*v, src)) {
434 out.keywords.push(("**".into(), v));
435 }
436 }
437 "list_splat" | "comment" => {}
438 _ => {
439 if a.kind() == "call" {
440 if let Some(p) = a.child_by_field_name("function").and_then(|f| path(f, src)) {
441 out.calls.push((out.positional.len(), p));
442 }
443 }
444 out.positional.push(value_expr(a, src));
445 }
446 }
447 }
448 out
449}
450
451fn value_expr(node: Node, src: &[u8]) -> Option<ValueExpr> {
453 match node.kind() {
454 "conditional_expression" => value_expr(*named_children(node).first()?, src),
456 "await" => value_expr(*named_children(node).first()?, src),
457 "string" => {
458 let mut parts = Vec::new();
459 for c in named_children(node) {
460 match c.kind() {
461 "string_content" => parts.push(TemplatePart::Lit(text(c, src).to_string())),
462 "interpolation" => parts.push(TemplatePart::Expr(
463 c.child_by_field_name("expression")
464 .and_then(|e| path(e, src)),
465 )),
466 _ => {}
467 }
468 }
469 if parts.iter().all(|p| matches!(p, TemplatePart::Lit(_))) {
470 let s: String = parts
471 .into_iter()
472 .map(|p| match p {
473 TemplatePart::Lit(s) => s,
474 TemplatePart::Expr(_) => String::new(),
475 })
476 .collect();
477 Some(ValueExpr::Lit(s))
478 } else {
479 Some(ValueExpr::Template(parts))
480 }
481 }
482 "concatenated_string" => {
483 let mut parts = Vec::new();
484 for c in named_children(node) {
485 match value_expr(c, src)? {
486 ValueExpr::Lit(s) => parts.push(TemplatePart::Lit(s)),
487 ValueExpr::Template(p) => parts.extend(p),
488 _ => return None,
489 }
490 }
491 Some(ValueExpr::Template(parts))
492 }
493 "subscript" => {
495 let value = node.child_by_field_name("value")?;
496 if path(value, src)?.join(".") != "os.environ" {
497 return None;
498 }
499 match value_expr(node.child_by_field_name("subscript")?, src)? {
500 ValueExpr::Lit(k) => Some(ValueExpr::Env(k)),
501 _ => None,
502 }
503 }
504 "call" => {
506 let f = path(node.child_by_field_name("function")?, src)?.join(".");
507 let list = named_children(node.child_by_field_name("arguments")?);
509 let attr = match (f.as_str(), list.as_slice()) {
510 ("getattr", [obj, name, ..]) => Some((*obj, *name)),
511 (t, [g, obj, name, ..])
512 if t.ends_with("to_thread") && text(*g, src) == "getattr" =>
513 {
514 Some((*obj, *name))
515 }
516 _ => None,
517 };
518 if let Some((obj, name)) = attr {
519 let (Some(mut p), Some(ValueExpr::Lit(n))) =
520 (path(obj, src), value_expr(name, src))
521 else {
522 return None;
523 };
524 p.push(n);
525 return Some(ValueExpr::Name(p));
526 }
527 if !matches!(
528 f.as_str(),
529 "os.getenv" | "getenv" | "os.environ.get" | "environ.get"
530 ) {
531 return call_value(node, src);
532 }
533 let first = named_children(node.child_by_field_name("arguments")?)
534 .into_iter()
535 .next()?;
536 match value_expr(first, src)? {
537 ValueExpr::Lit(k) => Some(ValueExpr::Env(k)),
538 _ => None,
539 }
540 }
541 "identifier" | "attribute" => path(node, src).map(ValueExpr::Name),
542 "binary_operator" => {
543 let sep = match field_text(node, "operator", src)? {
544 "/" => "/",
545 "+" => "",
546 _ => return None,
547 };
548 let mut parts = Vec::new();
549 for side in ["left", "right"] {
550 match value_expr(node.child_by_field_name(side)?, src)? {
551 ValueExpr::Join { sep: s, parts: p } if s == sep => parts.extend(p),
552 v => parts.push(v),
553 }
554 }
555 Some(ValueExpr::Join {
556 sep: sep.to_string(),
557 parts,
558 })
559 }
560 "dictionary" => Some(ValueExpr::Record(
561 named_children(node)
562 .into_iter()
563 .filter(|p| p.kind() == "pair")
564 .filter_map(|p| {
565 let ValueExpr::Lit(k) = value_expr(p.child_by_field_name("key")?, src)? else {
566 return None;
567 };
568 Some((k, value_expr(p.child_by_field_name("value")?, src)?))
569 })
570 .collect(),
571 )),
572 "parenthesized_expression" => value_expr(named_children(node).into_iter().next()?, src),
573 _ => None,
574 }
575}
576
577fn call_value(node: Node, src: &[u8]) -> Option<ValueExpr> {
579 let func = path(node.child_by_field_name("function")?, src)?;
580 let a = args(node.child_by_field_name("arguments")?, src);
581 Some(ValueExpr::Call {
582 func,
583 args: a.positional.into_iter().map_while(|v| v).collect(),
584 kwargs: a.keywords,
585 })
586}