Skip to main content

comindly_core/lang/
python.rs

1//! Python syntax adapter (tree-sitter-python).
2
3use 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 a.b, c as d — one Import per module so each keeps its alias
42            "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            // from [..]m import a, b as c / *
58            "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                // Everything after `*args` or a bare `*` is keyword-only.
166                "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
213/// `a.b` / `a.b as c` → (dotted module or name, alias).
214fn 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
225/// Dotted path of an identifier/attribute chain; `None` for anything dynamic.
226fn 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
238/// Path of a type annotation, unwrapping `Optional[X]`, `Annotated[X, …]` and `X | None`.
239fn 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        // Forward reference: `-> "Client"`.
255        "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
266// ---- contracts ----------------------------------------------------------------------
267
268const ROUTE_METHODS: &[&str] = &["get", "post", "put", "patch", "delete", "head", "options"];
269
270/// Constructors whose result can carry contract meaning (routers, apps, clients).
271const CTORS: &[&str] = &[
272    "APIRouter",
273    "FastAPI",
274    "Client",
275    "AsyncClient",
276    "Session",
277    "ClientSession",
278    "client",
279    "resource",
280];
281
282/// Calls that may be service operations; receivers decide at resolve time.
283const 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                // `…Router(prefix=…)`: possibly a subclass of a framework's router.
365                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    /// `name = v`, or `self.name = v` (returned as `self.name`; the walk files it on the class).
399    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            // `**params`: kept so keyword lookups can see into the dict it names.
434            "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
453/// What an expression evaluates to statically: literal, template, env var, or a name.
454fn value_expr(node: Node, src: &[u8]) -> Option<ValueExpr> {
455    match node.kind() {
456        // `a if cond else b`: the value when the condition holds.
457        "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        // os.environ["X"]
496        "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        // os.getenv("X", …) / os.environ.get("X", …); other calls kept for known callees.
507        "call" => {
508            let f = path(node.child_by_field_name("function")?, src)?.join(".");
509            // `getattr(obj, "x")`, also through `asyncio.to_thread(getattr, obj, "x")`.
510            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
579/// A call as a value: positional/keyword argument values by callee path.
580fn 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}