chidori_static_analysis/language/python/
parse.rs

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/// The ASTWalkContext structure represents the accumulated state during an Abstract Syntax Tree (AST) walk.
12///
13/// This context provides a way to track and evaluate references within the AST, allowing for filtered subsets of the AST to be built.
14///
15/// # Properties
16///
17/// * `context_stack_references`: A vector of vectors that stores the ContextPath values. Each inner vector represents a stack frame in the evaluation process.
18/// * `context_stack`: A vector that tracks the current context path.
19/// * `locals`: A set of strings representing local variables defined within the AST.
20/// * `local_contexts`: A vector of sets, where each set represents a separate local context.
21/// * `globals`: A set of strings representing global variables defined within the AST.
22#[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        // Clearing the existing locals
65        self.locals.clear();
66
67        // Union of all remaining hashsetss in local_contexts
68        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        // TODO: we need to check if this is a local variable or not
127        if self.var_exists(&name.to_string()) {
128            // true, the var exists in the local or global scope
129            self.context_stack
130                .push(ContextPath::IdentifierReferredTo{
131                    name: name.to_string(),
132                    in_scope: true,
133                    exposed: false
134                });
135        } else {
136            // TODO: this might not be last due to pattern assignments
137            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            // TODO: call expressions need to extract the identifier of the function being invoked
283            let ast::ExprCall {
284                func,
285                args,
286                keywords,
287                ..
288            } = expr;
289            // TODO: needs to be contained within the metadata of the expression
290            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            // TODO: this is a string interpolation and we need to handle internal references
301            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    // Add any remaining comments that may be in a block at the end of the code
383    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    // TODO: extract comments and associate them based on position relative to functions
393    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            // targets has multiple because you can multi assign
420            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                // TODO: this does include typeparams
440                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                // TODO: this does include typeparams
508                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                // No recursion needed
524            }
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                // TODO: here
536                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                    // Also traverse any pattern matching expressions if applicable
608                }
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                // Process try* statement with recursion as needed
636            }
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                // No recursion needed
652            }
653            ast::Stmt::ImportFrom(ast::StmtImportFrom { .. }) => {
654                // TODO: Import
655                // No recursion needed
656            }
657            ast::Stmt::Global(ast::StmtGlobal { .. }) => {
658                // No recursion needed
659            }
660            ast::Stmt::Nonlocal(ast::StmtNonlocal { .. }) => {
661                // No recursion needed
662            }
663            ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
664                traverse_expression(value, machine);
665            }
666            ast::Stmt::Pass(ast::StmtPass { .. }) => {
667                // No recursion needed
668            }
669            ast::Stmt::Break(ast::StmtBreak { .. }) => {
670                // No recursion needed
671            }
672            ast::Stmt::Continue(ast::StmtContinue { .. }) => {
673                // No recursion needed
674            }
675        }
676    }
677}
678
679pub fn build_report(context_paths: &Vec<Vec<ContextPath>>) -> Report {
680    // TODO: triggerable functions should note what they are triggered by
681    // TODO: for each of these we should store the context path that refers to them
682    // TODO: context paths should include spans
683    // TODO: for each of these we should store their type if its available
684    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 is the reversed order of the context path
691            encountered.push(context_path_unit);
692
693            // If we've declared a top level function, it is exposed
694            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                            // context_path: context_path.clone(),
701                            arguments: vec![],
702                            emit_event: vec![],
703                            trigger_on: vec![],
704                        });
705                }
706            }
707
708            // Function arguments get assigned to the triggerable function
709            if let ContextPath::FunctionArgument(name) = context_path_unit {
710                // traverse back through path until we hit the InFunction
711                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![], // Initialize with an empty string or a default value
719                                trigger_on: vec![],
720                            });
721                        x.arguments.push(name.clone());
722                    }
723                }
724            }
725
726            // If an identifier is referred to, and it has not been assigned to earlier during our interpreting
727            if let ContextPath::IdentifierReferredTo{name: identifier, exposed: false, in_scope: false} = context_path_unit {
728                // If we encounter both FunctionArguments and InFunction, then this is a function argument
729                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![], // Initialize with an empty string or a default value
739                                    trigger_on: vec![],
740                                });
741                            x.arguments.push(identifier.clone());
742                        }
743                    }
744                    continue;
745                }
746
747                // This is an exposed value if it does not occur inside the scope of a function
748                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                                // context_path: context_path.clone(),
758                            },
759                        );
760                        continue;
761                    }
762                }
763
764                // If this value is not being assigned to, then it is a dependency
765                if !encountered.contains(&&ContextPath::AssignmentToStatement) {
766                    depended_values.insert(
767                        identifier.clone(),
768                        ReportItem {
769                            // context_path: context_path.clone(),
770                        },
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        // TODO: fix
829        // assert_eq!(
830        //     context_stack_references,
831        //     vec![vec![ContextPath::VariableAssignment(String::from("x")),]]
832        // );
833    }
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(), // No data provided, initializing as empty
986            cell_depended_values: {
987                let mut map = std::collections::HashMap::new();
988                map.insert(
989                    "y".to_string(),
990                    ReportItem {
991                        // context_path: vec![
992                        //     ContextPath::InFunction("testing".to_string()),
993                        //     ContextPath::AssignmentFromStatement,
994                        //     ContextPath::IdentifierReferredTo("y".to_string(), false),
995                        // ],
996                    },
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                        // context_path: vec![ContextPath::InFunction("testing".to_string())],
1007                        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                        // context_path: vec![ContextPath::InFunction("fun_name".to_string())],
1069                        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(), // No data provided, initializing as empty
1101            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                        // context_path: vec![ContextPath::InFunction("testing".to_string())],
1109                        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(), // No data provided, initializing as empty
1199            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}