1use crate::language::{ChidoriStaticAnalysisError, InternalCallGraph, Report, ReportItem, ReportTriggerableFunctions, TextRange};
2use rustpython_parser::ast::{Constant, Expr, Identifier, Stmt};
3use rustpython_parser::{ast, Parse};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::collections::HashSet;
7use crate::language::ContextPath;
8
9
10
11#[derive(Default)]
23pub struct ASTWalkContext {
24 pub context_stack_references: Vec<Vec<ContextPath>>,
25 pub context_stack: Vec<ContextPath>,
26 pub locals: HashSet<String>,
27 pub local_contexts: Vec<HashSet<String>>,
28 pub globals: HashSet<String>,
29}
30
31impl ASTWalkContext {
32 fn new() -> Self {
33 Self {
34 context_stack_references: vec![],
35 context_stack: vec![],
36 locals: HashSet::new(),
37 local_contexts: vec![],
38 globals: HashSet::new(),
39 }
40 }
41
42 fn var_exists(&self, name: &str) -> bool {
43 if self.globals.contains(name) {
44 return true;
45 }
46 if self.locals.contains(name) {
47 return true;
48 }
49 for local_context in self.local_contexts.iter().rev() {
50 if local_context.contains(name) {
51 return true;
52 }
53 }
54 return false;
55 }
56
57 fn new_local_context(&mut self) {
58 self.local_contexts.push(self.locals.clone());
59 }
60
61 fn pop_local_context(&mut self) {
62 self.local_contexts.pop();
63
64 self.locals.clear();
66
67 self.locals.extend(
69 self.local_contexts
70 .iter()
71 .flat_map(|context| context.iter().cloned()),
72 );
73 }
74
75 fn enter_statement_class(&mut self, name: &Identifier) -> usize {
76 self.context_stack
77 .push(ContextPath::InClass(name.to_string()));
78 self.context_stack_references
79 .push(self.context_stack.clone());
80 self.context_stack.len()
81 }
82
83 fn enter_statement_function(&mut self, name: &Identifier, text_range: TextRange) -> usize {
84 self.context_stack
85 .push(ContextPath::InFunction(name.to_string(), text_range));
86 self.context_stack_references
87 .push(self.context_stack.clone());
88 self.context_stack.len()
89 }
90
91 fn enter_arguments(&mut self) -> usize {
92 self.context_stack
93 .push(ContextPath::FunctionArguments);
94 self.context_stack.len()
95 }
96
97 fn encounter_argument(&mut self, name: &Identifier) {
98 self.locals.insert(name.to_string());
99 self.context_stack
100 .push(ContextPath::FunctionArgument(name.to_string()));
101 self.context_stack_references
102 .push(self.context_stack.clone());
103 }
104
105 fn enter_decorator_expression(&mut self, idx: &usize) -> usize {
106 self.context_stack
107 .push(ContextPath::InFunctionDecorator(idx.clone()));
108 self.context_stack.len()
109 }
110
111 fn enter_call_expression(&mut self) -> usize {
112 self.context_stack.push(ContextPath::InCallExpression);
113 self.context_stack.len()
114 }
115
116 fn encounter_constant(&mut self, name: &Constant) {
117 if let Constant::Str(s) = name {
118 self.context_stack
119 .push(ContextPath::Constant(s.to_string()));
120 self.context_stack_references
121 .push(self.context_stack.clone());
122 }
123 }
124
125 fn encounter_named_reference(&mut self, name: &Identifier) {
126 if self.var_exists(&name.to_string()) {
128 self.context_stack
130 .push(ContextPath::IdentifierReferredTo{
131 name: name.to_string(),
132 in_scope: true,
133 exposed: false
134 });
135 } else {
136 if let Some(ContextPath::AssignmentToStatement) = self.context_stack.last() {
138 self.locals.insert(name.to_string());
139 }
140 if let Some(ContextPath::FunctionArguments) = self.context_stack.last() {
141 self.locals.insert(name.to_string());
142 }
143 self.context_stack
144 .push(ContextPath::IdentifierReferredTo{
145 name: name.to_string(),
146 in_scope: false,
147 exposed: false
148 });
149 }
150 self.context_stack_references
151 .push(self.context_stack.clone());
152 self.context_stack.pop();
153 }
154
155 fn enter_assignment_to_statement(&mut self) -> usize {
156 self.context_stack.push(ContextPath::AssignmentToStatement);
157 self.context_stack.len()
158 }
159
160 fn enter_assignment_from_statement(&mut self) -> usize {
161 self.context_stack
162 .push(ContextPath::AssignmentFromStatement);
163 self.context_stack.len()
164 }
165
166 fn enter_attr(&mut self, name: &Identifier) -> usize {
167 self.context_stack
168 .push(ContextPath::Attribute(name.to_string()));
169 self.context_stack.len()
170 }
171
172 fn pop_until(&mut self, size: usize) {
173 while self.context_stack.len() >= size {
174 self.context_stack.pop();
175 }
176 }
177}
178
179fn traverse_expression(expr: &ast::Expr, machine: &mut ASTWalkContext) {
180 match expr {
181 ast::Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
182 for value in values {
183 traverse_expression(value, machine);
184 }
185 }
186 ast::Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => {
187 traverse_expression(value, machine);
188 }
189 ast::Expr::BinOp(expr) => {
190 let ast::ExprBinOp { left, right, .. } = expr;
191 traverse_expression(left, machine);
192 traverse_expression(right, machine);
193 }
194 ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => {
195 traverse_expression(operand, machine);
196 }
197 ast::Expr::Lambda(ast::ExprLambda { body, .. }) => {
198 traverse_expression(body, machine);
199 }
200 ast::Expr::IfExp(ast::ExprIfExp {
201 test, body, orelse, ..
202 }) => {
203 traverse_expression(test, machine);
204 traverse_expression(body, machine);
205 traverse_expression(orelse, machine);
206 }
207 ast::Expr::Dict(ast::ExprDict { keys, values, .. }) => {
208 for key in keys {
209 if let Some(key) = key {
210 traverse_expression(key, machine);
211 }
212 }
213 for value in values {
214 traverse_expression(value, machine);
215 }
216 }
217 ast::Expr::Set(ast::ExprSet { elts, .. }) => {
218 for elt in elts {
219 traverse_expression(elt, machine);
220 }
221 }
222 ast::Expr::ListComp(ast::ExprListComp {
223 elt, generators, ..
224 }) => {
225 traverse_expression(elt, machine);
226 for generator in generators {
227 traverse_comprehension(generator, machine);
228 }
229 }
230 ast::Expr::SetComp(ast::ExprSetComp {
231 elt, generators, ..
232 }) => {
233 traverse_expression(elt, machine);
234 for generator in generators {
235 traverse_comprehension(generator, machine);
236 }
237 }
238 ast::Expr::DictComp(ast::ExprDictComp {
239 key,
240 value,
241 generators,
242 ..
243 }) => {
244 traverse_expression(key, machine);
245 traverse_expression(value, machine);
246 for generator in generators {
247 traverse_comprehension(generator, machine);
248 }
249 }
250 ast::Expr::GeneratorExp(ast::ExprGeneratorExp {
251 elt, generators, ..
252 }) => {
253 traverse_expression(elt, machine);
254 for generator in generators {
255 traverse_comprehension(generator, machine);
256 }
257 }
258 ast::Expr::Await(ast::ExprAwait { value, .. }) => {
259 traverse_expression(value, machine);
260 }
261 ast::Expr::Yield(ast::ExprYield { value, .. }) => {
262 if let Some(val) = value {
263 traverse_expression(val, machine);
264 }
265 }
266 ast::Expr::YieldFrom(ast::ExprYieldFrom { value, .. }) => {
267 traverse_expression(value, machine);
268 }
269 ast::Expr::Compare(ast::ExprCompare {
270 left,
271 ops,
272 comparators,
273 ..
274 }) => {
275 traverse_expression(left, machine);
276 for comparator in comparators {
277 traverse_expression(comparator, machine);
278 }
279 }
280 ast::Expr::Call(expr) => {
281 let idx = machine.enter_call_expression();
282 let ast::ExprCall {
284 func,
285 args,
286 keywords,
287 ..
288 } = expr;
289 for arg in args {
291 traverse_expression(arg, machine);
292 }
293 for keyword in keywords {
294 traverse_expression(&keyword.value, machine);
295 }
296 traverse_expression(func, machine);
297 machine.pop_until(idx);
298 }
299 ast::Expr::FormattedValue(ast::ExprFormattedValue { value, .. }) => {
300 traverse_expression(value, machine);
302 }
303 ast::Expr::JoinedStr(ast::ExprJoinedStr { values, .. }) => {
304 for value in values {
305 traverse_expression(value, machine);
306 }
307 }
308 ast::Expr::Constant(ast::ExprConstant { value, .. }) => {
309 machine.encounter_constant(value);
310 }
311 ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
312 let x = machine.enter_attr(attr);
313 traverse_expression(value, machine);
314 machine.pop_until(x);
315 }
316 ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
317 traverse_expression(value, machine);
318 traverse_expression(slice, machine);
319 }
320 ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
321 traverse_expression(value, machine);
322 }
323 ast::Expr::Name(ast::ExprName { id, .. }) => {
324 machine.encounter_named_reference(id);
325 }
326 ast::Expr::List(ast::ExprList { elts, .. }) => {
327 for elt in elts {
328 traverse_expression(elt, machine);
329 }
330 }
331 ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
332 for elt in elts {
333 traverse_expression(elt, machine);
334 }
335 }
336 ast::Expr::Slice(ast::ExprSlice {
337 lower, upper, step, ..
338 }) => {
339 if let Some(expr) = lower {
340 traverse_expression(expr, machine);
341 }
342 if let Some(expr) = upper {
343 traverse_expression(expr, machine);
344 }
345 if let Some(expr) = step {
346 traverse_expression(expr, machine);
347 }
348 }
349 }
350}
351
352fn extract_python_comments(code: &str) -> Vec<String> {
353 let mut comments = Vec::new();
354 let mut current_block = Vec::new();
355 let lines = code.lines();
356
357 for line in lines {
358 let trimmed_line = line.trim();
359 if let Some(pos) = trimmed_line.find('#') {
360 let is_start_of_line = pos == 0;
361 let comment = trimmed_line[pos..].trim_start_matches('#').trim().to_string();
362
363 if !comment.is_empty() {
364 if is_start_of_line {
365 current_block.push(comment);
366 } else {
367 if !current_block.is_empty() {
368 comments.push(current_block.join("\n"));
369 current_block.clear();
370 }
371 comments.push(comment);
372 }
373 }
374 } else {
375 if !current_block.is_empty() {
376 comments.push(current_block.join("\n"));
377 current_block.clear();
378 }
379 }
380 }
381
382 if !current_block.is_empty() {
384 comments.push(current_block.join("\n"));
385 }
386
387 comments
388}
389
390
391pub fn extract_dependencies_python(source_code: &str) -> Result<Vec<Vec<ContextPath>>, ChidoriStaticAnalysisError> {
392 let mut comments = extract_python_comments(source_code);
394 let ast = ast::Suite::parse(source_code, "<embedded>")
395 .map_err(|e| {
396 ChidoriStaticAnalysisError::ParseError {
397 msg: e.error.to_string(),
398 offset: e.offset.to_u32(),
399 source_path: e.source_path,
400 source_code: source_code.to_string(),
401 }
402 })?;
403 let mut machine = ASTWalkContext::default();
404 traverse_statements(&ast, &mut machine);
405 Ok(machine.context_stack_references)
406}
407
408fn traverse_comprehension(comp: &ast::Comprehension, machine: &mut ASTWalkContext) {
409 traverse_expression(&comp.target, machine);
410 traverse_expression(&comp.iter, machine);
411 for condition in &comp.ifs {
412 traverse_expression(condition, machine);
413 }
414}
415
416pub fn traverse_statements(statements: &[ast::Stmt], machine: &mut ASTWalkContext) {
417 for stmt in statements {
418 match stmt {
419 ast::Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
421 let idx = machine.enter_assignment_to_statement();
422 for target in targets {
423 traverse_expression(target, machine);
424 }
425 machine.pop_until(idx);
426 let idx = machine.enter_assignment_from_statement();
427 traverse_expression(value, machine);
428 machine.pop_until(idx);
429 }
430 ast::Stmt::FunctionDef(ast::StmtFunctionDef {
431 body,
432 decorator_list,
433 name,
434 args,
435 range,
436 ..
437 }) => {
438 machine.globals.insert(name.to_string());
439 machine.new_local_context();
441 let idx = machine.enter_statement_function(name, TextRange {
442 start: range.start().to_usize(),
443 end: range.end().to_usize()
444 });
445 for (i, decorator) in decorator_list.iter().enumerate() {
446 let idx = machine.enter_decorator_expression(&i);
447 traverse_expression(decorator, machine);
448 machine.pop_until(idx);
449 }
450 let args_idx = machine.enter_arguments();
451 for ast::ArgWithDefault {
452 range,
453 def,
454 default,
455 ..
456 } in &args.args
457 {
458 if let ast::Arg { arg, .. } = def {
459 machine.encounter_named_reference(arg);
460 }
461 }
462 machine.pop_until(args_idx);
463 traverse_statements(body, machine);
464 machine.pop_until(idx);
465 machine.pop_local_context();
466 }
467
468 ast::Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
469 body,
470 decorator_list,
471 name,
472 args,
473 range,
474 ..
475 }) => {
476 machine.globals.insert(name.to_string());
477 machine.new_local_context();
478 let idx = machine.enter_statement_function(name, TextRange {
479 start: range.start().to_usize(),
480 end: range.end().to_usize()
481 });
482 for (i, decorator) in decorator_list.iter().enumerate() {
483 let idx = machine.enter_decorator_expression(&i);
484 traverse_expression(decorator, machine);
485 machine.pop_until(idx);
486 }
487 let args_idx = machine.enter_arguments();
488 for ast::ArgWithDefault {
489 range,
490 def,
491 default,
492 ..
493 } in &args.args
494 {
495 if let ast::Arg { arg, .. } = def {
496 machine.encounter_named_reference(arg);
497 }
498 }
499 machine.pop_until(args_idx);
500 traverse_statements(body, machine);
501 machine.pop_until(idx);
502 machine.pop_local_context();
503 }
504 ast::Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) => {
505 machine.globals.insert(name.to_string());
506 let idx = machine.enter_statement_class(name);
507 traverse_statements(body, machine);
509 machine.pop_until(idx);
510 machine.pop_local_context();
511 }
512 ast::Stmt::Return(ast::StmtReturn { value, .. }) => {
513 if let Some(expr) = value {
514 traverse_expression(expr, machine);
515 }
516 }
517 ast::Stmt::Delete(ast::StmtDelete { targets, .. }) => {
518 for target in targets {
519 traverse_expression(target, machine);
520 }
521 }
522 ast::Stmt::TypeAlias(ast::StmtTypeAlias { .. }) => {
523 }
525 ast::Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
526 traverse_expression(target, machine);
527 traverse_expression(value, machine);
528 }
529 ast::Stmt::AnnAssign(ast::StmtAnnAssign {
530 target,
531 annotation,
532 value,
533 ..
534 }) => {
535 traverse_expression(target, machine);
537 traverse_expression(annotation, machine);
538 if let Some(expr) = value {
539 traverse_expression(expr, machine);
540 }
541 }
542 ast::Stmt::For(ast::StmtFor {
543 target,
544 iter,
545 body,
546 orelse,
547 ..
548 }) => {
549 let idx = machine.enter_assignment_to_statement();
550 traverse_expression(target, machine);
551 machine.pop_until(idx);
552 traverse_expression(target, machine);
553 traverse_expression(iter, machine);
554 traverse_statements(body, machine);
555 traverse_statements(orelse, machine);
556 }
557 ast::Stmt::AsyncFor(ast::StmtAsyncFor {
558 target,
559 iter,
560 body,
561 orelse,
562 ..
563 }) => {
564 let idx = machine.enter_assignment_to_statement();
565 traverse_expression(target, machine);
566 machine.pop_until(idx);
567 traverse_expression(iter, machine);
568 traverse_statements(body, machine);
569 traverse_statements(orelse, machine);
570 }
571 ast::Stmt::While(ast::StmtWhile {
572 test, body, orelse, ..
573 }) => {
574 traverse_expression(test, machine);
575 traverse_statements(body, machine);
576 traverse_statements(orelse, machine);
577 }
578 ast::Stmt::If(ast::StmtIf {
579 test, body, orelse, ..
580 }) => {
581 traverse_expression(test, machine);
582 traverse_statements(body, machine);
583 traverse_statements(orelse, machine);
584 }
585 ast::Stmt::With(ast::StmtWith { items, body, .. }) => {
586 for item in items {
587 traverse_expression(&item.context_expr, machine);
588 if let Some(expr) = &item.optional_vars {
589 traverse_expression(&expr, machine);
590 }
591 }
592 traverse_statements(body, machine);
593 }
594 ast::Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
595 for item in items {
596 traverse_expression(&item.context_expr, machine);
597 if let Some(expr) = &item.optional_vars {
598 traverse_expression(&expr, machine);
599 }
600 }
601 traverse_statements(body, machine);
602 }
603 ast::Stmt::Match(ast::StmtMatch { subject, cases, .. }) => {
604 traverse_expression(subject, machine);
605 for case in cases {
606 traverse_statements(&case.body, machine);
607 }
609 }
610 ast::Stmt::Raise(ast::StmtRaise { exc, cause, .. }) => {
611 if let Some(expr) = exc {
612 traverse_expression(expr, machine);
613 }
614 if let Some(expr) = cause {
615 traverse_expression(expr, machine);
616 }
617 }
618 ast::Stmt::Try(ast::StmtTry {
619 body,
620 handlers,
621 orelse,
622 finalbody,
623 ..
624 }) => {
625 traverse_statements(body, machine);
626 for handler in handlers {
627 if let ast::ExceptHandler::ExceptHandler(handler) = handler {
628 traverse_statements(&handler.body, machine);
629 }
630 }
631 traverse_statements(orelse, machine);
632 traverse_statements(finalbody, machine);
633 }
634 ast::Stmt::TryStar(ast::StmtTryStar { .. }) => {
635 }
637 ast::Stmt::Assert(ast::StmtAssert { test, msg, .. }) => {
638 traverse_expression(test, machine);
639 if let Some(expr) = msg {
640 traverse_expression(expr, machine);
641 }
642 }
643 ast::Stmt::Import(ast::StmtImport { names, .. }) => {
644 for name in names {
645 if let Some(name) = &name.asname {
646 machine.globals.insert(name.to_string());
647 } else {
648 machine.globals.insert(name.name.to_string());
649 }
650 }
651 }
653 ast::Stmt::ImportFrom(ast::StmtImportFrom { .. }) => {
654 }
657 ast::Stmt::Global(ast::StmtGlobal { .. }) => {
658 }
660 ast::Stmt::Nonlocal(ast::StmtNonlocal { .. }) => {
661 }
663 ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
664 traverse_expression(value, machine);
665 }
666 ast::Stmt::Pass(ast::StmtPass { .. }) => {
667 }
669 ast::Stmt::Break(ast::StmtBreak { .. }) => {
670 }
672 ast::Stmt::Continue(ast::StmtContinue { .. }) => {
673 }
675 }
676 }
677}
678
679pub fn build_report(context_paths: &Vec<Vec<ContextPath>>) -> Report {
680 let mut exposed_values = HashMap::new();
685 let mut depended_values = HashMap::new();
686 let mut triggerable_functions = HashMap::new();
687 for context_path in context_paths {
688 let mut encountered = vec![];
689 for (idx, context_path_unit) in context_path.iter().enumerate() {
690 encountered.push(context_path_unit);
692
693 if let ContextPath::InFunction(name, _) = context_path_unit {
695 if !triggerable_functions.contains_key(name) {
696 triggerable_functions
697 .entry(name.clone())
698 .or_insert_with(|| ReportTriggerableFunctions {
699
700 arguments: vec![],
702 emit_event: vec![],
703 trigger_on: vec![],
704 });
705 }
706 }
707
708 if let ContextPath::FunctionArgument(name) = context_path_unit {
710 let clone_path = context_path.clone();
712 for (idx, context_path_unit) in (clone_path.into_iter()).rev().enumerate() {
713 if let ContextPath::InFunction(function_name, _) = context_path_unit {
714 let mut x = triggerable_functions
715 .entry(function_name.clone())
716 .or_insert_with(|| ReportTriggerableFunctions {
717 arguments: vec![],
718 emit_event: vec![], trigger_on: vec![],
720 });
721 x.arguments.push(name.clone());
722 }
723 }
724 }
725
726 if let ContextPath::IdentifierReferredTo{name: identifier, exposed: false, in_scope: false} = context_path_unit {
728 if encountered.iter().any(|x| matches!(x, ContextPath::InFunction(_, _)))
730 && encountered.iter().any(|x| matches!(x, ContextPath::FunctionArguments))
731 {
732 for context_path_unit in &encountered {
733 if let ContextPath::InFunction(function_name, _) = context_path_unit {
734 let mut x = triggerable_functions
735 .entry(function_name.clone())
736 .or_insert_with(|| ReportTriggerableFunctions {
737 arguments: vec![],
738 emit_event: vec![], trigger_on: vec![],
740 });
741 x.arguments.push(identifier.clone());
742 }
743 }
744 continue;
745 }
746
747 if encountered
749 .iter()
750 .find(|x| matches!(x, ContextPath::InFunction(_, _)))
751 .is_none()
752 {
753 if encountered.contains(&&ContextPath::AssignmentToStatement) {
754 exposed_values.insert(
755 identifier.clone(),
756 ReportItem {
757 },
759 );
760 continue;
761 }
762 }
763
764 if !encountered.contains(&&ContextPath::AssignmentToStatement) {
766 depended_values.insert(
767 identifier.clone(),
768 ReportItem {
769 },
771 );
772 continue;
773 }
774 }
775 }
776 }
777
778
779 let py_built_ins: HashSet<&str> = [
780 "__name__", "type", "abs", "all", "any", "ascii", "bin", "bool", "breakpoint", "bytearray",
781 "bytes", "callable", "chr", "classmethod", "compile", "complex", "delattr", "dict", "dir",
782 "divmod", "enumerate", "eval", "exec", "filter", "float", "format", "frozenset", "getattr",
783 "globals", "hasattr", "hash", "help", "hex", "id", "input", "int", "isinstance", "issubclass",
784 "iter", "len", "list", "locals", "map", "max", "memoryview", "min", "next", "object", "oct",
785 "open", "ord", "pow", "print", "property", "range", "repr", "reversed", "round", "set", "setattr",
786 "slice", "sorted", "staticmethod", "str", "sum", "super", "tuple", "type", "vars", "zip"
787 ].iter().cloned().collect();
788
789 depended_values.retain(|value,_ | !py_built_ins.contains(value.as_str()));
790
791 Report {
792 internal_call_graph: InternalCallGraph {
793 graph: Default::default(),
794 },
795 cell_exposed_values: exposed_values,
796 cell_depended_values: depended_values,
797 triggerable_functions: triggerable_functions,
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use indoc::indoc;
805
806 #[test]
807 fn test_extraction_of_ch_statements() {
808 let python_source = indoc! { r#"
809 from chidori.core import ch
810
811 ch.prompt.configure("default", ch.llm(model="openai"))
812 "#};
813 let context_stack_references = extract_dependencies_python(python_source).unwrap();
814 insta::with_settings!({
815 description => python_source,
816 omit_expression => true
817 }, {
818 insta::assert_yaml_snapshot!(context_stack_references);
819 });
820 }
821
822 #[test]
823 fn test_assignment_to_value() {
824 let python_source = indoc! { r#"
825 x = 1
826 "#};
827 let context_stack_references = extract_dependencies_python(python_source);
828 }
834
835 #[test]
836 fn test_nothing_extracted_with_no_ch_references() {
837 let python_source = indoc! { r#"
838 def create_dockerfile():
839 return prompt("prompts/create_dockerfile")
840 "#};
841 let context_stack_references = extract_dependencies_python(python_source).unwrap();
842 insta::with_settings!({
843 description => python_source,
844 omit_expression => true
845 }, {
846 insta::assert_yaml_snapshot!(context_stack_references);
847 });
848 }
849
850 #[test]
851 fn test_function_decorator_ch_annotation() {
852 let python_source = indoc! { r#"
853 @ch.register()
854 def migration_agent():
855 ch.set("bar", 1)
856 "#};
857 let context_stack_references = extract_dependencies_python(python_source).unwrap();
858 insta::with_settings!({
859 description => python_source,
860 omit_expression => true
861 }, {
862 insta::assert_yaml_snapshot!(context_stack_references);
863 });
864 }
865
866 #[test]
867 fn test_function_decorator_ch_annotation_with_internal_ch_and_emit() {
868 let python_source = indoc! { r#"
869 @ch.on_event("new_file")
870 @ch.emit_as("file_created")
871 def dispatch_agent(ev):
872 ch.set("file_path", ev.file_path)
873 "#};
874 let context_stack_references = extract_dependencies_python(python_source).unwrap();
875 insta::with_settings!({
876 description => python_source,
877 omit_expression => true
878 }, {
879 insta::assert_yaml_snapshot!(context_stack_references);
880 });
881 }
882
883 #[test]
884 fn test_ch_reference_internal_to_function() {
885 let python_source = indoc! { r#"
886 def evaluate_agent(ev):
887 ch.set("file_path", ev.file_path)
888 migration_agent()
889 "#};
890 let context_stack_references = extract_dependencies_python(python_source).unwrap();
891 insta::with_settings!({
892 description => python_source,
893 omit_expression => true
894 }, {
895 insta::assert_yaml_snapshot!(context_stack_references);
896 });
897 }
898
899 #[test]
900 fn test_ch_function_decoration_referring_to_another_function() {
901 let python_source = indoc! { r#"
902 @ch.p(create_dockerfile)
903 def setup_pipeline(x):
904 return x
905 "#};
906 let context_stack_references = extract_dependencies_python(python_source).unwrap();
907
908 insta::assert_yaml_snapshot!(context_stack_references);
909
910 }
911
912 #[test]
913 fn test_classes_are_identified() {
914 let python_source = indoc! { r#"
915 import unittest
916
917 class TestMarshalledValues(unittest.TestCase):
918 def test_addTwo(self):
919 self.assertEqual(addTwo(2), 4)
920
921 unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(TestMarshalledValues))
922
923 "#};
924 let context_stack_references = extract_dependencies_python(python_source).unwrap();
925
926 insta::with_settings!({
927 description => python_source,
928 omit_expression => true
929 }, {
930 insta::assert_yaml_snapshot!(context_stack_references);
931 });
932
933 }
934
935
936
937 #[test]
938 fn test_for_loop_assignments_are_captured() {
939 let python_source = indoc! { r#"
940 async def run_prompt(number_of_states):
941 out = ""
942 for state in (await get_states_first_letters(num=number_of_states)).split('\n'):
943 out += await first_letter(state)
944 return "demo" + out
945 "#};
946 let context_stack_references = extract_dependencies_python(python_source).unwrap();
947 insta::assert_yaml_snapshot!(context_stack_references);
948 }
949
950 #[test]
951 fn test_pipe_function_composition() {
952 let python_source = indoc! { r#"
953 def main():
954 bar() | foo() | baz()
955 "#};
956 let context_stack_references = extract_dependencies_python(python_source).unwrap();
957 insta::with_settings!({
958 description => python_source,
959 omit_expression => true
960 }, {
961 insta::assert_yaml_snapshot!(context_stack_references);
962 });
963 }
964
965 #[test]
966 fn test_report_generation() {
967 let python_source = indoc! { r#"
968 def testing():
969 x = 2 + y
970 return x
971 "#};
972 let context_stack_references = extract_dependencies_python(python_source).unwrap();
973 insta::with_settings!({
974 description => python_source,
975 omit_expression => true
976 }, {
977 insta::assert_yaml_snapshot!(context_stack_references);
978 });
979 let result = build_report(&context_stack_references);
980 let report = Report {
981
982 internal_call_graph: InternalCallGraph {
983 graph: Default::default(),
984 },
985 cell_exposed_values: std::collections::HashMap::new(), cell_depended_values: {
987 let mut map = std::collections::HashMap::new();
988 map.insert(
989 "y".to_string(),
990 ReportItem {
991 },
997 );
998 map
999 },
1000 triggerable_functions: {
1001 let mut map = std::collections::HashMap::new();
1002 map.insert(
1003 "testing".to_string(),
1004 ReportTriggerableFunctions {
1005
1006 arguments: vec![],
1008 emit_event: vec![],
1009 trigger_on: vec![],
1010 },
1011 );
1012 map
1013 },
1014 };
1015
1016 assert_eq!(result, report);
1017 }
1018
1019 #[test]
1020 fn test_report_generation_with_import() -> anyhow::Result<()> {
1021 let python_source = indoc! { r#"
1022import random
1023
1024def fun_name():
1025 w = function_that_doesnt_exist()
1026 v = 5
1027 return v
1028
1029x = random.randint(0, 10)
1030"#};
1031 let context_stack_references = extract_dependencies_python(python_source).map_err(|e| anyhow::Error::msg(format!("{:?}", e)))?;
1032 insta::with_settings!({
1033 description => python_source,
1034 omit_expression => true
1035 }, {
1036 insta::assert_yaml_snapshot!(context_stack_references);
1037 });
1038 let result = build_report(&context_stack_references);
1039 let report = Report {
1040
1041 internal_call_graph: InternalCallGraph {
1042 graph: Default::default(),
1043 },
1044 cell_exposed_values: {
1045 let mut map = std::collections::HashMap::new();
1046 map.insert(
1047 "x".to_string(),
1048 ReportItem {
1049 },
1050 );
1051 map
1052 },
1053 cell_depended_values: {
1054 let mut map = std::collections::HashMap::new();
1055 map.insert(
1056 "function_that_doesnt_exist".to_string(),
1057 ReportItem {
1058 },
1059 );
1060 map
1061 },
1062 triggerable_functions: {
1063 let mut map = std::collections::HashMap::new();
1064 map.insert(
1065 "fun_name".to_string(),
1066 ReportTriggerableFunctions {
1067
1068 arguments: vec![],
1070 emit_event: vec![],
1071 trigger_on: vec![],
1072 },
1073 );
1074 map
1075 },
1076 };
1077 assert_eq!(result, report);
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn test_report_generation_function_with_arguments() -> anyhow::Result<()> {
1083 let python_source = indoc! { r#"
1084 async def complex_args_function(a, b, c=2, d=3):
1085 return a + b + c + d
1086 "#};
1087 let context_stack_references = extract_dependencies_python(python_source).map_err(|e| anyhow::Error::msg(format!("{:?}", e)))?;
1088 insta::with_settings!({
1089 description => python_source,
1090 omit_expression => true
1091 }, {
1092 insta::assert_yaml_snapshot!(context_stack_references);
1093 });
1094 let result = build_report(&context_stack_references);
1095 let report = Report {
1096
1097 internal_call_graph: InternalCallGraph {
1098 graph: Default::default(),
1099 },
1100 cell_exposed_values: std::collections::HashMap::new(), cell_depended_values: std::collections::HashMap::new(),
1102 triggerable_functions: {
1103 let mut map = std::collections::HashMap::new();
1104 map.insert(
1105 "complex_args_function".to_string(),
1106 ReportTriggerableFunctions {
1107
1108 arguments: vec!["a", "b", "c", "d"].into_iter().map(|a| a.to_string()).collect(),
1110 emit_event: vec![],
1111 trigger_on: vec![],
1112 },
1113 );
1114 map
1115 },
1116 };
1117
1118 assert_eq!(result, report);
1119 Ok(())
1120 }
1121
1122 #[test]
1123 fn test_report_generation_for_loop_variable_assignment() -> anyhow::Result<()> {
1124 let python_source = indoc! { r#"
1125 async def run_prompt(number_of_states):
1126 out = ""
1127 for state in (await get_states_first_letters(num=number_of_states)).split('\n'):
1128 out += await first_letter(state)
1129 return "demo" + out
1130 "#};
1131 let context_stack_references = extract_dependencies_python(python_source).map_err(|e| anyhow::Error::msg(format!("{:?}", e)))?;
1132 insta::with_settings!({
1133 sort_maps => true,
1134 description => python_source,
1135 omit_expression => true
1136 }, {
1137 insta::assert_yaml_snapshot!(context_stack_references);
1138 });
1139 let result = build_report(&context_stack_references);
1140
1141 insta::with_settings!({
1142 sort_maps => true,
1143 description => python_source,
1144 omit_expression => true
1145 }, {
1146 insta::assert_yaml_snapshot!(result);
1147 });
1148
1149 Ok(())
1150 }
1151
1152 #[test]
1153 fn test_reference_to_undeclared_function() -> anyhow::Result<()> {
1154 let python_source = indoc! { r#"
1155 out = await read_file_and_load_to_memory("./")
1156 "#};
1157 let context_stack_references = extract_dependencies_python(python_source).map_err(|e| anyhow::Error::msg(format!("{:?}", e)))?;
1158 insta::with_settings!({
1159 description => python_source,
1160 omit_expression => true
1161 }, {
1162 insta::assert_yaml_snapshot!(context_stack_references);
1163 });
1164 let result = build_report(&context_stack_references);
1165 insta::with_settings!({
1166 description => python_source,
1167 omit_expression => true
1168 }, {
1169 insta::assert_yaml_snapshot!(result);
1170 });
1171 Ok(())
1172 }
1173
1174 #[test]
1175 fn test_report_generation_with_class() -> anyhow::Result<()> {
1176 let python_source = indoc! { r#"
1177 import unittest
1178
1179 class TestMarshalledValues(unittest.TestCase):
1180 def test_addTwo(self):
1181 self.assertEqual(addTwo(2), 4)
1182
1183 unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(TestMarshalledValues))
1184 "#};
1185 let context_stack_references = extract_dependencies_python(python_source).map_err(|e| anyhow::Error::msg(format!("{:?}", e)))?;
1186 insta::with_settings!({
1187 description => python_source,
1188 omit_expression => true
1189 }, {
1190 insta::assert_yaml_snapshot!(context_stack_references);
1191 });
1192 let result = build_report(&context_stack_references);
1193 let report = Report {
1194
1195 internal_call_graph: InternalCallGraph {
1196 graph: Default::default(),
1197 },
1198 cell_exposed_values: std::collections::HashMap::new(), cell_depended_values: {
1200 let mut map = std::collections::HashMap::new();
1201 map.insert(
1202 "addTwo".to_string(),
1203 ReportItem {
1204 },
1205 );
1206 map
1207 },
1208 triggerable_functions: {
1209 let mut map = std::collections::HashMap::new();
1210 map.insert(
1211 "test_addTwo".to_string(),
1212 ReportTriggerableFunctions {
1213 arguments: vec!["self".to_string()],
1214 emit_event: vec![],
1215 trigger_on: vec![],
1216 },
1217 );
1218 map
1219 },
1220 };
1221
1222 assert_eq!(result, report);
1223 Ok(())
1224 }
1225}