chidori_static_analysis/language/javascript/
parse.rs

1extern crate swc_ecma_parser;
2
3use crate::language::javascript::parse::ContextPath::Constant;
4use crate::language::{InternalCallGraph, python, TextRange};
5use crate::language::{Report, ReportItem, ReportTriggerableFunctions};
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8use std::env::var;
9use swc_common::sync::Lrc;
10use swc_common::{
11    errors::{ColorConfig, Handler},
12    FileName, FilePathMapping, SourceMap,
13};
14use swc_common::source_map::Pos;
15use swc_ecma_ast as ast;
16use swc_ecma_ast::{BlockStmtOrExpr, Callee, Decl, Expr, FnDecl, ForHead, Ident, ImportSpecifier, Lit, MemberProp, ModuleDecl, ModuleItem, Pat, PatOrExpr, PropName, Stmt};
17use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
18use crate::language::ChidoriStaticAnalysisError;
19use crate::language::ContextPath;
20
21fn remove_hash_and_numbers(input: &str) -> String {
22    match input.find('#') {
23        Some(index) => input[..index].to_string(),
24        None => input.to_string(),
25    }
26}
27
28/// The ASTWalkContext structure represents the accumulated state during an Abstract Syntax Tree (AST) walk.
29///
30/// This context provides a way to track and evaluate references within the AST, allowing for filtered subsets of the AST to be built.
31///
32/// # Properties
33///
34/// * `context_stack_references`: A vector of vectors that stores the ContextPath values. Each inner vector represents a stack frame in the evaluation process.
35/// * `context_stack`: A vector that tracks the current context path.
36/// * `locals`: A set of strings representing local variables defined within the AST.
37/// * `local_contexts`: A vector of sets, where each set represents a separate local context.
38/// * `globals`: A set of strings representing global variables defined within the AST.
39#[derive(Default)]
40pub struct ASTWalkContext {
41    pub context_stack_references: Vec<Vec<ContextPath>>,
42    pub context_stack: Vec<ContextPath>,
43    pub locals: HashSet<String>,
44    pub local_contexts: Vec<HashSet<String>>,
45    pub globals: HashSet<String>,
46}
47
48impl ASTWalkContext {
49    fn new() -> Self {
50        Self {
51            context_stack_references: vec![],
52            context_stack: vec![],
53            locals: HashSet::new(),
54            local_contexts: vec![],
55            globals: HashSet::new(),
56        }
57    }
58
59    fn var_exists(&self, name: &str) -> bool {
60        if self.globals.contains(name) {
61            return true;
62        }
63        if self.locals.contains(name) {
64            return true;
65        }
66        for local_context in self.local_contexts.iter().rev() {
67            if local_context.contains(name) {
68                return true;
69            }
70        }
71        return false;
72    }
73
74    fn new_local_context(&mut self) {
75        self.local_contexts.push(self.locals.clone());
76    }
77
78    fn insert_local(&mut self, ident: &ast::Ident) {
79        let name = remove_hash_and_numbers(&ident.to_string());
80        self.locals.insert(name);
81    }
82
83    fn pop_local_context(&mut self) {
84        // Clearing the existing locals
85        self.locals.clear();
86
87        // Union of all remaining hashsets in local_contexts
88        // restoring the parent local context
89        self.locals.extend(
90            self.local_contexts
91                .iter()
92                .flat_map(|context| context.iter().cloned()),
93        );
94
95        self.local_contexts.pop();
96    }
97
98    fn enter_anonymous_function(&mut self) -> usize {
99        self.context_stack.push(ContextPath::InAnonFunction);
100        // self.context_stack_references
101        //     .push(self.context_stack.clone());
102        self.context_stack.len()
103    }
104
105    fn enter_statement_function(&mut self, name: &ast::Ident, range: TextRange) -> usize {
106        let name = remove_hash_and_numbers(&name.to_string());
107        self.context_stack.push(ContextPath::InFunction(name, range));
108        self.context_stack.len()
109    }
110
111    fn enter_params(&mut self) -> usize {
112        self.context_stack.push(ContextPath::FunctionArguments);
113        self.context_stack.len()
114    }
115
116    fn enter_decorator_expression(&mut self, idx: &usize) -> usize {
117        self.context_stack
118            .push(ContextPath::InFunctionDecorator(idx.clone()));
119        self.context_stack.len()
120    }
121
122    fn enter_call_expression(&mut self) -> usize {
123        self.context_stack.push(ContextPath::InCallExpression);
124        self.context_stack.len()
125    }
126
127    fn encounter_constant(&mut self, lit: &ast::Lit) {
128        match lit {
129            Lit::Str(ast::Str { value, .. }) => {
130                self.context_stack
131                    .push(ContextPath::Constant(value.to_string()));
132                // self.context_stack_references
133                //     .push(self.context_stack.clone());
134            }
135            Lit::Bool(_) => {}
136            Lit::Null(_) => {}
137            Lit::Num(_) => {}
138            Lit::BigInt(_) => {}
139            Lit::Regex(_) => {}
140            Lit::JSXText(_) => {}
141        }
142    }
143
144    fn encounter_named_reference(&mut self, name: &ast::Ident) {
145        let name = remove_hash_and_numbers(&name.to_string());
146        let mut var_exists = false;
147        if self.globals.contains(&name.to_string()) {
148            var_exists = true;
149        }
150        if self.locals.contains(&name.to_string()) {
151            var_exists = true;
152        }
153        for local_context in self.local_contexts.iter().rev() {
154            if local_context.contains(&name.to_string()) {
155                var_exists = true;
156            }
157        }
158
159        if var_exists {
160            // true, the var already exists in the local or global scope
161            self.context_stack
162                .push(ContextPath::IdentifierReferredTo{
163                    name: name.to_string(),
164                    in_scope: true,
165                    exposed: self.local_contexts.len() == 0
166                });
167        } else {
168            if self.context_stack.contains(&ContextPath::AssignmentToStatement) {
169                self.locals.insert(name.to_string());
170            }
171            // if let Some(ContextPath::AssignmentToStatement) = self.context_stack.last() {
172            //     self.locals.insert(name.to_string());
173            // }
174            if self.context_stack.contains(&ContextPath::FunctionArguments) {
175                self.locals.insert(name.to_string());
176                self.context_stack
177                    .push(ContextPath::IdentifierReferredTo{
178                        name: name.to_string(),
179                        in_scope: false,
180                        exposed: self.local_contexts.len() == 0
181                });
182                return;
183            }
184            self.context_stack
185                .push(ContextPath::IdentifierReferredTo{
186                    name: name.to_string(),
187                    in_scope: false,
188                    exposed: self.local_contexts.len() == 0
189                });
190        }
191    }
192
193    fn enter_assignment_to_statement(&mut self) -> usize {
194        self.context_stack.push(ContextPath::AssignmentToStatement);
195        self.context_stack.len()
196    }
197
198    fn enter_assignment_from_statement(&mut self) -> usize {
199        self.context_stack
200            .push(ContextPath::AssignmentFromStatement);
201        self.context_stack.len()
202    }
203
204    fn enter_attr(&mut self, name: &ast::Ident) -> usize {
205        let name = remove_hash_and_numbers(&name.to_string());
206        self.context_stack
207            .push(ContextPath::Attribute(name.to_string()));
208        self.context_stack.len()
209    }
210
211    fn pop_until(&mut self, size: usize) {
212        self.context_stack_references
213            .push(self.context_stack.clone());
214        while self.context_stack.len() >= size {
215            self.context_stack.pop();
216        }
217    }
218}
219
220fn traverse_module(module: ModuleItem, machine: &mut ASTWalkContext) {
221    match module {
222        ModuleItem::ModuleDecl(mod_decl) => match mod_decl {
223            ModuleDecl::Import(ast::ImportDecl { specifiers, .. }) => {
224                for specifier in specifiers {
225                    match specifier {
226                        ImportSpecifier::Named(ast::ImportNamedSpecifier { local, .. }) => {
227                            let name = remove_hash_and_numbers(&local.to_string());
228                            machine.globals.insert(name);
229                        }
230                        ImportSpecifier::Default(ast::ImportDefaultSpecifier { local, .. }) => {
231                            let name = remove_hash_and_numbers(&local.to_string());
232                            machine.globals.insert(name);
233                        }
234                        ImportSpecifier::Namespace(ast::ImportStarAsSpecifier {
235                            local, ..
236                        }) => {
237                            let name = remove_hash_and_numbers(&local.to_string());
238                            machine.globals.insert(name);
239                        }
240                    }
241                }
242            }
243            ModuleDecl::ExportDecl(ast::ExportDecl { .. }) => {}
244            ModuleDecl::ExportNamed(ast::NamedExport { .. }) => {}
245            ModuleDecl::ExportDefaultDecl(ast::ExportDefaultDecl { .. }) => {}
246            ModuleDecl::ExportDefaultExpr(ast::ExportDefaultExpr { .. }) => {}
247            ModuleDecl::ExportAll(ast::ExportAll { .. }) => {}
248            ModuleDecl::TsImportEquals(_) => {}
249            ModuleDecl::TsExportAssignment(_) => {}
250            ModuleDecl::TsNamespaceExport(_) => {}
251        },
252        ModuleItem::Stmt(stmt) => {
253            let v = vec![stmt];
254            traverse_stmts(v.as_slice(), machine);
255        }
256    }
257}
258
259fn traverse_prop_name(prop_name: &PropName, machine: &mut ASTWalkContext) {
260    match prop_name {
261        PropName::Ident(name) => {
262            // machine.encounter_named_reference(name);
263        }
264        PropName::Str(_) => {}
265        PropName::Num(_) => {}
266        PropName::Computed(_) => {}
267        PropName::BigInt(_) => {}
268    }
269}
270
271fn traverse_pat(pat: &ast::Pat, machine: &mut ASTWalkContext) {
272    match pat {
273        Pat::Ident(ast::BindingIdent { id, .. }) => {
274            machine.encounter_named_reference(id);
275        }
276        Pat::Array(ast::ArrayPat { elems, .. }) => {
277            for elem in elems {
278                if let Some(elem) = elem {
279                    traverse_pat(elem, machine);
280                }
281            }
282        }
283        Pat::Rest(ast::RestPat { arg, .. }) => {
284            traverse_pat(arg, machine);
285        }
286        Pat::Object(ast::ObjectPat { props, .. }) => {
287            for prop in props {
288                match prop {
289                    ast::ObjectPatProp::KeyValue(ast::KeyValuePatProp { key, value, .. }) => {
290                        // Key to access
291                        traverse_prop_name(key, machine);
292                        // Assignment patterns
293                        traverse_pat(value, machine);
294                    }
295                    ast::ObjectPatProp::Assign(ast::AssignPatProp { key, value, .. }) => {
296                        machine.encounter_named_reference(key);
297                        if let Some(value) = value {
298                            traverse_expr(value, machine);
299                        }
300                    }
301                    ast::ObjectPatProp::Rest(ast::RestPat { arg, .. }) => {
302                        traverse_pat(arg, machine);
303                    }
304                }
305            }
306        }
307        Pat::Assign(ast::AssignPat { left, right, .. }) => {
308            traverse_pat(left, machine);
309            traverse_expr(right, machine);
310        }
311        Pat::Invalid(ast::Invalid { .. }) => {}
312        Pat::Expr(expr) => {
313            traverse_expr(expr, machine);
314        }
315    }
316}
317
318fn traverse_expr(expr: &ast::Expr, machine: &mut ASTWalkContext) {
319    match expr {
320        Expr::This(ast::ThisExpr { .. }) => {}
321        Expr::Array(ast::ArrayLit { elems, .. }) => {}
322        Expr::Object(ast::ObjectLit { props, .. }) => {}
323        Expr::Fn(ast::FnExpr { .. }) => {}
324        Expr::Unary(ast::UnaryExpr { arg, .. }) => {
325            traverse_expr(arg, machine);
326        }
327        Expr::Update(ast::UpdateExpr { arg, .. }) => {
328            traverse_expr(arg, machine);
329        }
330        Expr::Bin(ast::BinExpr { left, right, .. }) => {
331            traverse_expr(left, machine);
332            traverse_expr(right, machine);
333        }
334        Expr::Assign(ast::AssignExpr {
335            op, left, right, ..
336        }) => {
337            let idx = machine.enter_assignment_to_statement();
338            match left {
339                PatOrExpr::Expr(expr) => {
340                    traverse_expr(expr, machine);
341                }
342                PatOrExpr::Pat(pat) => {
343                    traverse_pat(pat, machine);
344                }
345            }
346            machine.pop_until(idx);
347            let idx = machine.enter_assignment_from_statement();
348            traverse_expr(right, machine);
349            machine.pop_until(idx);
350        }
351        Expr::Member(ast::MemberExpr { obj, prop, .. }) => {
352            match prop {
353                MemberProp::Ident(id) => {
354                    machine.enter_attr(id);
355                },
356                MemberProp::PrivateName(ast::PrivateName { id, .. }) => {
357                    machine.enter_attr(id);
358                }
359                MemberProp::Computed(ast::ComputedPropName { expr, .. }) => {
360                    // TODO: handle computed prop name
361                }
362            };
363            traverse_expr(&obj, machine);
364            // machine.pop_until(idx);
365        }
366        Expr::SuperProp(ast::SuperPropExpr { .. }) => {}
367        Expr::Cond(ast::CondExpr {
368            test, cons, alt, ..
369        }) => {
370            traverse_expr(&test, machine);
371            traverse_expr(&cons, machine);
372            traverse_expr(&alt, machine);
373        }
374        Expr::Call(ast::CallExpr { args, callee, .. }) => {
375            let idx = machine.enter_call_expression();
376            match callee {
377                Callee::Super(_) => {}
378                Callee::Import(_) => {}
379                Callee::Expr(expr) => {
380                    traverse_expr(expr, machine);
381                }
382            }
383
384            for arg in args {
385                traverse_expr(&arg.expr, machine);
386            }
387            machine.pop_until(idx);
388        }
389        Expr::New(ast::NewExpr { callee, args, .. }) => {
390            let idx = machine.enter_call_expression();
391            traverse_expr(&callee, machine);
392            if let Some(args) = args {
393                for arg in args {
394                    traverse_expr(&arg.expr, machine);
395                }
396            }
397            machine.pop_until(idx);
398        }
399        Expr::Seq(ast::SeqExpr { exprs, .. }) => {
400            for expr in exprs {
401                traverse_expr(expr, machine);
402            }
403        }
404        Expr::Ident(id) => {
405            machine.encounter_named_reference(id);
406        }
407        Expr::Lit(lit) => {
408            machine.encounter_constant(lit);
409        }
410        Expr::Tpl(ast::Tpl { exprs, .. }) => {
411            for expr in exprs {
412                traverse_expr(&expr, machine);
413            }
414        }
415        Expr::TaggedTpl(ast::TaggedTpl { tag, .. }) => {
416            traverse_expr(&tag, machine);
417        }
418        Expr::Arrow(ast::ArrowExpr { params, body, .. }) => {
419            let idx = machine.enter_anonymous_function();
420            machine.new_local_context();
421            let params_idx = machine.enter_params();
422            for param in params {
423                traverse_pat(param, machine);
424            }
425            machine.pop_until(params_idx);
426            match **body {
427                BlockStmtOrExpr::Expr(ref expr) => {
428                    traverse_expr(expr, machine);
429                }
430                BlockStmtOrExpr::BlockStmt(ref block) => {
431                    traverse_stmts(&block.stmts, machine);
432                }
433            }
434            machine.pop_local_context();
435            machine.pop_until(idx);
436        }
437        Expr::Class(ast::ClassExpr { class, .. }) => {
438            // TODO: parse class
439        }
440        Expr::Yield(ast::YieldExpr { arg, .. }) => {
441            if let Some(arg) = arg {
442                traverse_expr(arg, machine);
443            }
444        }
445        Expr::MetaProp(ast::MetaPropExpr { .. }) => {}
446        Expr::Await(ast::AwaitExpr { arg, .. }) => {
447            traverse_expr(arg, machine);
448        }
449        Expr::Paren(ast::ParenExpr { expr, .. }) => {
450            traverse_expr(&expr, machine);
451        }
452        Expr::JSXMember(ast::JSXMemberExpr { .. }) => {}
453        Expr::JSXNamespacedName(ast::JSXNamespacedName { .. }) => {}
454        Expr::JSXEmpty(ast::JSXEmptyExpr { .. }) => {}
455        Expr::JSXElement(el) => {}
456        Expr::JSXFragment(ast::JSXFragment { .. }) => {}
457        Expr::TsTypeAssertion(ast::TsTypeAssertion { .. }) => {}
458        Expr::TsConstAssertion(ast::TsConstAssertion { .. }) => {}
459        Expr::TsNonNull(ast::TsNonNullExpr { .. }) => {}
460        Expr::TsAs(ast::TsAsExpr { .. }) => {}
461        Expr::TsInstantiation(ast::TsInstantiation { .. }) => {}
462        Expr::TsSatisfies(ast::TsSatisfiesExpr { .. }) => {}
463        Expr::PrivateName(ast::PrivateName { .. }) => {}
464        Expr::OptChain(ast::OptChainExpr { .. }) => {}
465        Expr::Invalid(ast::Invalid { .. }) => {}
466    }
467}
468fn traverse_stmt(stmt: &Stmt, machine: &mut ASTWalkContext) {
469    match stmt {
470        Stmt::Expr(expr_stmt) => {
471            traverse_expr(&*expr_stmt.expr, machine);
472        }
473        Stmt::Block(block_stmt) => {
474            traverse_stmts(&block_stmt.stmts, machine);
475        }
476        Stmt::Empty(_) => {}
477        Stmt::Debugger(ast::DebuggerStmt { .. }) => {}
478        Stmt::With(ast::WithStmt { obj, body, .. }) => {
479            traverse_expr(&obj, machine);
480            traverse_stmt(&body, machine);
481        }
482        Stmt::Return(ast::ReturnStmt { arg, .. }) => {
483            if let Some(arg) = arg {
484                traverse_expr(&arg, machine);
485            }
486        }
487        Stmt::Labeled(ast::LabeledStmt { body, .. }) => {
488            traverse_stmt(&body, machine);
489        }
490        Stmt::Break(ast::BreakStmt { .. }) => {}
491        Stmt::Continue(ast::ContinueStmt { .. }) => {}
492        Stmt::If(ast::IfStmt {
493            test, cons, alt, ..
494        }) => {
495            traverse_expr(&test, machine);
496            traverse_stmt(&cons, machine);
497            if let Some(alt) = alt {
498                traverse_stmt(&alt, machine);
499            }
500        }
501        Stmt::Switch(ast::SwitchStmt {
502            discriminant,
503            cases,
504            ..
505        }) => {
506            traverse_expr(&discriminant, machine);
507            for case in cases {
508                if let Some(test) = &case.test {
509                    traverse_expr(&test, machine);
510                }
511                traverse_stmts(&case.cons, machine);
512            }
513        }
514        Stmt::Throw(ast::ThrowStmt { arg, .. }) => {
515            traverse_expr(&arg, machine);
516        }
517        Stmt::Try(x) => {
518            let ast::TryStmt {
519                block,
520                handler,
521                finalizer,
522                ..
523            } = &**x;
524            traverse_stmts(&block.stmts, machine);
525        }
526        Stmt::While(ast::WhileStmt { test, body, .. }) => {
527            machine.new_local_context();
528            traverse_expr(&test, machine);
529            traverse_stmt(&body, machine);
530            machine.pop_local_context();
531        }
532        Stmt::DoWhile(ast::DoWhileStmt { test, body, .. }) => {
533            machine.new_local_context();
534            traverse_expr(&test, machine);
535            traverse_stmt(&body, machine);
536            machine.pop_local_context();
537        }
538        Stmt::For(ast::ForStmt {
539            init,
540            test,
541            body,
542            update,
543            ..
544        }) => {
545            machine.new_local_context();
546            if let Some(init) = init {
547                match init {
548                    ast::VarDeclOrExpr::VarDecl(x) => {
549                        let ast::VarDecl { decls, .. } = &**x;
550                        for decl in decls {
551                            let idx = machine.enter_assignment_to_statement();
552                            traverse_pat(&decl.name, machine);
553                            machine.pop_until(idx);
554                            if let Some(init) = &decl.init {
555                                traverse_expr(&init, machine);
556                            }
557                        }
558                    }
559                    ast::VarDeclOrExpr::Expr(expr) => {
560                        traverse_expr(&expr, machine);
561                    }
562                }
563            }
564            if let Some(test) = test {
565                traverse_expr(&test, machine);
566            }
567            if let Some(update) = update {
568                traverse_expr(&update, machine);
569            }
570            traverse_stmt(&body, machine);
571            machine.pop_local_context();
572        }
573        Stmt::ForIn(ast::ForInStmt {
574            left, right, body, ..
575        }) => {
576            machine.new_local_context();
577            traverse_for_statement(machine, left);
578            traverse_expr(&right, machine);
579            traverse_stmt(&body, machine);
580            machine.pop_local_context();
581        }
582        Stmt::ForOf(ast::ForOfStmt {
583            left, right, body, ..
584        }) => {
585            machine.new_local_context();
586            traverse_for_statement(machine, left);
587            traverse_expr(&right, machine);
588            traverse_stmt(&body, machine);
589            machine.pop_local_context();
590        }
591        Stmt::Decl(decl) => match decl {
592            Decl::Class(_) => {}
593            Decl::Fn(ast::FnDecl {
594                ident, function, ..
595            }) => {
596                machine.insert_local(ident);
597                let ast::Function { params, body, span, .. } = &**function;
598                let idx = machine.enter_statement_function(ident, TextRange {
599                    start: span.lo.to_usize(),
600                    end: span.hi.to_usize(),
601                });
602                let params_idx = machine.enter_params();
603                for param in params {
604                    traverse_pat(&param.pat, machine);
605                }
606                machine.pop_until(params_idx);
607                if let Some(body) = body {
608                    traverse_stmts(&body.stmts, machine);
609                }
610                machine.pop_until(idx);
611            }
612            Decl::Var(v) => {
613                let ast::VarDecl { decls, .. } = &**v;
614                for decl in decls {
615                    let idx = machine.enter_assignment_to_statement();
616                    traverse_pat(&decl.name, machine);
617                    machine.pop_until(idx);
618                    if let Some(init) = &decl.init {
619                        let idx = machine.enter_assignment_from_statement();
620                        traverse_expr(&init, machine);
621                        machine.pop_until(idx);
622                    }
623                }
624            }
625            Decl::Using(v) => {
626                let ast::UsingDecl { decls, .. } = &**v;
627                for decl in decls {
628                    let idx = machine.enter_assignment_to_statement();
629                    traverse_pat(&decl.name, machine);
630                    machine.pop_until(idx);
631                    if let Some(init) = &decl.init {
632                        traverse_expr(&init, machine);
633                    }
634                }
635            }
636            Decl::TsInterface(_) => {}
637            Decl::TsTypeAlias(_) => {}
638            Decl::TsEnum(_) => {}
639            Decl::TsModule(_) => {}
640        },
641    }
642}
643
644fn traverse_for_statement(machine: &mut ASTWalkContext, left: &ForHead) {
645    match left {
646        ForHead::VarDecl(x) => {
647            let ast::VarDecl { decls, .. } = &**x;
648            for decl in decls {
649                let idx = machine.enter_assignment_to_statement();
650                traverse_pat(&decl.name, machine);
651                machine.pop_until(idx);
652                if let Some(init) = &decl.init {
653                    traverse_expr(&init, machine);
654                }
655            }
656        }
657        ForHead::UsingDecl(x) => {
658            let ast::UsingDecl { decls, .. } = &**x;
659            for decl in decls {
660                let idx = machine.enter_assignment_to_statement();
661                traverse_pat(&decl.name, machine);
662                machine.pop_until(idx);
663                if let Some(init) = &decl.init {
664                    traverse_expr(&init, machine);
665                }
666            }
667        }
668        ForHead::Pat(x) => {
669            traverse_pat(&x, machine);
670        }
671    }
672}
673
674fn traverse_stmts(stmts: &[Stmt], machine: &mut ASTWalkContext) {
675    for stmt in stmts {
676        traverse_stmt(stmt, machine);
677    }
678}
679
680pub fn extract_dependencies_js(source: &str) -> Result<Vec<Vec<ContextPath>>, ChidoriStaticAnalysisError> {
681    let mut machine = ASTWalkContext::new();
682    let cm: Lrc<SourceMap> = Default::default();
683    let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));
684    let fm = cm.new_source_file(FileName::Custom("test.js".into()), source.to_string());
685
686    let parse_module = |syntax: Syntax| {
687        let lexer = Lexer::new(
688            syntax,
689            Default::default(),
690            StringInput::from(&*fm),
691            None,
692        );
693
694        let mut parser = Parser::new_from(lexer);
695
696        for e in parser.take_errors() {
697            e.into_diagnostic(&handler).emit();
698        }
699
700        parser.parse_module()
701    };
702
703    let module = parse_module(Syntax::Es(Default::default()))
704        .or_else(|_| parse_module(Syntax::Typescript(Default::default())))
705        .map_err(|mut e| {
706            // Unrecoverable fatal error occurred
707            e
708            // e.into_diagnostic(&handler).emit()
709        });
710
711    match module {
712        Ok(module) => {
713            for item in module.body {
714                traverse_module(item, &mut machine);
715            }
716            Ok(machine.context_stack_references)
717        },
718        Err(e) => {
719            Err(ChidoriStaticAnalysisError::ParseError {
720                msg: format!("{:?}", e),
721                offset: 0,
722                source_path: "".to_string(),
723                source_code: "".to_string(),
724            })
725
726        }
727    }
728}
729
730
731pub fn build_report(context_paths: &Vec<Vec<ContextPath>>) -> Report {
732    let mut exposed_values = HashMap::new();
733    let mut depended_values = HashMap::new();
734    let mut triggerable_functions = HashMap::new();
735    for context_path in context_paths {
736        let mut encountered = vec![];
737
738        let mut in_function: Option<&String> = None;
739        let mut in_call_expression = false;
740        let mut attribute_path = vec![];
741
742        for (idx, context_path_unit) in context_path.iter().enumerate() {
743            encountered.push(context_path_unit);
744
745            // If we've declared a top level function, it is exposed
746            if let ContextPath::InFunction(name, _) = context_path_unit {
747                in_function = Some(name);
748                if !triggerable_functions.contains_key(name) {
749                    triggerable_functions
750                        .entry(name.clone())
751                        .or_insert_with(|| ReportTriggerableFunctions {
752                            arguments: vec![],
753                            emit_event: vec![],
754                            trigger_on: vec![],
755                        });
756                }
757            }
758
759            if let Some(in_function_name) = in_function {
760                if let ContextPath::InCallExpression = context_path_unit {
761                    in_call_expression = true;
762                }
763
764                if in_call_expression {
765                    if let ContextPath::Attribute(attr) = context_path_unit {
766                        attribute_path.push(attr);
767                    }
768                }
769
770                if context_path.len() - 1 == idx {
771                    let mut x = triggerable_functions
772                        .entry(in_function_name.clone())
773                        .or_insert_with(|| ReportTriggerableFunctions {
774
775                            arguments: vec![],
776                            emit_event: vec![], // Initialize with an empty string or a default value
777                            trigger_on: vec![],
778                        });
779
780                    if attribute_path == vec![&"emitAs".to_string()] {
781                        if let ContextPath::Constant(const_name) = encountered[idx] {
782                            x.emit_event.push(const_name.clone());
783                        }
784                    }
785
786                    if attribute_path == vec![&"onEvent".to_string()] {
787                        if let ContextPath::Constant(const_name) = encountered[idx] {
788                            x.trigger_on.push(const_name.clone());
789                        }
790                    }
791                }
792            }
793
794            // TODO: this needs to be updated to be similar to the python implementation
795            if let ContextPath::IdentifierReferredTo{name: identifier, exposed, in_scope: false} = context_path_unit {
796                // If this value is not being assigned to, then it is a dependency
797
798                if encountered.iter().any(|x| matches!(x, ContextPath::InFunction(_, _)))
799                    && encountered.iter().any(|x| matches!(x, ContextPath::FunctionArguments))
800                {
801                    for context_path_unit in &encountered {
802                        if let ContextPath::InFunction(function_name, _) = context_path_unit {
803                            let mut x = triggerable_functions
804                                .entry(function_name.clone())
805                                .or_insert_with(|| ReportTriggerableFunctions {
806                                    arguments: vec![],
807                                    emit_event: vec![], // Initialize with an empty string or a default value
808                                    trigger_on: vec![],
809                                });
810                            x.arguments.push(identifier.clone());
811                        }
812                    }
813                    continue;
814                }
815
816                // This is an exposed value if it does not occur inside the scope of a function
817                if encountered
818                    .iter()
819                    .find(|x| matches!(x, ContextPath::InFunction(_, _)))
820                    .is_none()
821                    &&
822                    encountered
823                        .iter()
824                        .find(|x| matches!(x, ContextPath::InAnonFunction))
825                        .is_none()
826                    && *exposed
827
828                {
829                    if encountered.contains(&&ContextPath::AssignmentToStatement) {
830                        exposed_values.insert(
831                            identifier.clone(),
832                            ReportItem {
833                                // context_path: context_path.clone(),
834                            },
835                        );
836                        continue;
837                    }
838                }
839
840                if !encountered.contains(&&ContextPath::AssignmentToStatement) && !encountered.contains(&&ContextPath::FunctionArguments) {
841                    depended_values.insert(
842                        identifier.clone(),
843                        ReportItem {
844                            // context_path: context_path.clone(),
845                        },
846                    );
847                    continue;
848                }
849            }
850        }
851    }
852
853    let js_built_ins: HashSet<&str> = [
854        "Deno", "Chidori",  "Array", "ArrayBuffer", "Boolean", "DataView", "Date", "Error", "EvalError", "Float32Array",
855        "Float64Array", "Function", "Generator", "GeneratorFunction", "Infinity", "Int8Array",
856        "Int16Array", "Int32Array", "InternalError", "Intl", "JSON", "Map", "Math", "NaN",
857        "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect",
858        "RegExp", "Set", "SharedArrayBuffer", "String", "Symbol", "SyntaxError", "TypeError",
859        "URIError", "Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "WeakMap",
860        "WeakSet", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape",
861        "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "unescape", "uneval", "setTimeout", "setInterval", "console", "process"
862    ].iter().cloned().collect();
863
864    depended_values.retain(|value,_ | !js_built_ins.contains(value.as_str()));
865
866    Report {
867        internal_call_graph: InternalCallGraph {
868            graph: Default::default(),
869        },
870        cell_exposed_values: exposed_values,
871        cell_depended_values: depended_values,
872        triggerable_functions: triggerable_functions,
873    }
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879    use crate::language::python::parse::extract_dependencies_python;
880    use crate::language::{Report, ReportItem, ReportTriggerableFunctions};
881    use indoc::indoc;
882
883    #[test]
884    fn test_extraction_of_ch_statements() {
885        let js_source = indoc! { r#"
886            import * as ch from "@1kbirds/chidori";
887
888            ch.prompt.configure("default", ch.llm({model: "openai"}))
889            "#};
890        let context_stack_references = extract_dependencies_js(js_source).unwrap();
891        insta::assert_yaml_snapshot!(context_stack_references);
892    }
893
894    #[test]
895    fn test_assignment_to_value() {
896        let js_source = indoc! { r#"
897            const x = 1
898            "#};
899        let context_stack_references = extract_dependencies_js(js_source).unwrap();
900        insta::with_settings!({
901            description => js_source,
902            omit_expression => true
903        }, {
904            insta::assert_yaml_snapshot!(context_stack_references);
905        });
906    }
907
908    #[test]
909    fn test_nothing_extracted_with_no_ch_references() {
910        let js_source = indoc! { r#"
911            function createDockerfile() {
912                return prompt("prompts/create_dockerfile")
913            }
914            "#};
915        let context_stack_references = extract_dependencies_js(js_source).unwrap();
916        insta::with_settings!({
917            description => js_source,
918            omit_expression => true
919        }, {
920            insta::assert_yaml_snapshot!(context_stack_references);
921        });
922    }
923
924    #[test]
925    fn test_handling_react_hook_style_refrence() {
926        let js_source = indoc! { r#"
927            function createDockerfile() {
928                useHook(() => {
929                   ch.prompt("demo");
930                }, [otherFunction]);
931                return prompt("prompts/create_dockerfile")
932            }
933            "#};
934        let context_stack_references = extract_dependencies_js(js_source).unwrap();
935
936        insta::with_settings!({
937            description => js_source,
938            omit_expression => true
939        }, {
940            insta::assert_yaml_snapshot!(context_stack_references);
941        });
942
943    }
944
945    #[test]
946    fn test_function_decorator_ch_annotation() {
947        let js_source = indoc! { r#"
948            function migrationAgent() {
949                ch.register();
950                ch.set("bar", 1);
951            }
952            "#};
953        let context_stack_references = extract_dependencies_js(js_source).unwrap();
954        insta::with_settings!({
955            description => js_source,
956            omit_expression => true
957        }, {
958            insta::assert_yaml_snapshot!(context_stack_references);
959        });
960    }
961
962    #[test]
963    fn test_function_decorator_ch_annotation_with_internal_ch_and_emit() {
964        let js_source = indoc! { r#"
965            function dispatch_agent(ev) {
966                ch.onEvent("new_file")
967                ch.emitAs("file_created")
968                ch.emitAs("file_created", "multiple", "args")
969                ch.set("file_path", ev.file_path)
970            }
971            "#};
972        let context_stack_references = extract_dependencies_js(js_source).unwrap();
973
974        insta::with_settings!({
975            description => js_source,
976            omit_expression => true
977        }, {
978            insta::assert_yaml_snapshot!(context_stack_references);
979        });
980
981    }
982
983    #[test]
984    fn test_ch_reference_internal_to_function() {
985        let js_source = indoc! { r#"
986            function evaluate_agent(ev) {
987                ch.set("file_path", ev.file_path)
988                migration_agent()
989            }
990            "#};
991        let context_stack_references = extract_dependencies_js(js_source).unwrap();
992
993        insta::with_settings!({
994            description => js_source,
995            omit_expression => true
996        }, {
997            insta::assert_yaml_snapshot!(context_stack_references);
998        });
999
1000    }
1001
1002    #[test]
1003    fn test_ch_function_decoration_referring_to_another_function() {
1004        let js_source = indoc! { r#"
1005            function setupPipeline(x) {
1006                ch.p(create_dockerfile)
1007                return x
1008            }
1009            "#};
1010        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1011
1012        insta::with_settings!({
1013            description => js_source,
1014            omit_expression => true
1015        }, {
1016            insta::assert_yaml_snapshot!(context_stack_references);
1017        });
1018
1019    }
1020
1021    #[test]
1022    fn test_ch_function_with_arguments() {
1023        let js_source = indoc! { r#"
1024            function subtract(a, b) {
1025                return a - b;
1026            }
1027
1028            // Example usage
1029            const v = subtract(x, 5);
1030            "#};
1031        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1032
1033        insta::with_settings!({
1034            description => js_source,
1035            omit_expression => true
1036        }, {
1037            insta::assert_yaml_snapshot!(context_stack_references);
1038        });
1039
1040    }
1041
1042    #[test]
1043    fn test_pipe_function_composition() {
1044        let js_source = indoc! { r#"
1045            function main() {
1046                bar() | foo() | baz()
1047            }
1048            "#};
1049        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1050        insta::with_settings!({
1051            description => js_source,
1052            omit_expression => true
1053        }, {
1054            insta::assert_yaml_snapshot!(context_stack_references);
1055        });
1056    }
1057
1058    #[test]
1059    fn test_report_generation() {
1060        let js_source = indoc! { r#"
1061        function testing() {
1062            ch.onEvent("new_file");
1063            ch.emitAs("file_created");
1064            const x = 2 + y;
1065            return x
1066        }
1067            "#};
1068        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1069        insta::with_settings!({
1070            description => js_source,
1071            omit_expression => true
1072        }, {
1073            insta::assert_yaml_snapshot!(context_stack_references);
1074        });
1075        let result = build_report(&context_stack_references);
1076        insta::with_settings!({
1077            description => js_source,
1078            omit_expression => true
1079        }, {
1080            insta::assert_yaml_snapshot!(result);
1081        });
1082    }
1083
1084    #[test]
1085    fn test_report_for_simple_function() {
1086        let js_source = indoc! { r#"
1087        function testing(x) {
1088            return x
1089        }
1090            "#};
1091        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1092        insta::with_settings!({
1093            description => js_source,
1094            omit_expression => true
1095        }, {
1096            insta::assert_yaml_snapshot!(context_stack_references);
1097        });
1098        let result = build_report(&context_stack_references);
1099        insta::with_settings!({
1100            description => js_source,
1101            omit_expression => true
1102        }, {
1103            insta::assert_yaml_snapshot!(result);
1104        });
1105    }
1106
1107    #[test]
1108    fn test_report_generation_deno_test_framework() {
1109        let js_source = indoc! { r#"
1110        import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
1111
1112        Deno.test("addition test", async () => {
1113            const result = await add_two(2);
1114            console.log(result);
1115            assertEquals(result, 4);
1116        });
1117        "#};
1118        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1119        insta::with_settings!({
1120            description => js_source,
1121            omit_expression => true
1122        }, {
1123            insta::assert_yaml_snapshot!(context_stack_references);
1124        });
1125        ;
1126        let result = build_report(&context_stack_references);
1127        let report = Report {
1128            internal_call_graph: InternalCallGraph {
1129                graph: Default::default(),
1130            },
1131            cell_exposed_values: {
1132                let mut map = std::collections::HashMap::new();
1133                map
1134            },
1135            cell_depended_values: {
1136                let mut map = std::collections::HashMap::new();
1137                map.insert("add_two".to_string(), ReportItem {});
1138                map
1139            },
1140            triggerable_functions: {
1141                let mut map = std::collections::HashMap::new();
1142                map
1143            },
1144        };
1145        assert_eq!(result, report);
1146    }
1147
1148    #[test]
1149    fn test_report_generation_with_import() {
1150        let js_source = indoc! { r#"
1151import { random } from "random"
1152
1153function fun_name() {
1154    const w = function_that_doesnt_exist()
1155    const v = 5
1156    return v
1157}
1158
1159x = random.randint(0, 10)
1160"#};
1161        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1162        insta::with_settings!({
1163            description => js_source,
1164            omit_expression => true
1165        }, {
1166            insta::assert_yaml_snapshot!(context_stack_references);
1167        });
1168        ;
1169        let result = build_report(&context_stack_references);
1170        let report = Report {
1171
1172            internal_call_graph: InternalCallGraph {
1173                graph: Default::default(),
1174            },
1175            cell_exposed_values: {
1176                let mut map = std::collections::HashMap::new();
1177                map.insert("x".to_string(), ReportItem {});
1178                map
1179            },
1180            cell_depended_values: {
1181                let mut map = std::collections::HashMap::new();
1182                map.insert("function_that_doesnt_exist".to_string(), ReportItem {});
1183                map
1184            },
1185            triggerable_functions: {
1186                let mut map = std::collections::HashMap::new();
1187                map.insert(
1188                    "fun_name".to_string(),
1189                    ReportTriggerableFunctions {
1190
1191                        arguments: vec![],
1192                        emit_event: vec![],
1193                        trigger_on: vec![],
1194                    },
1195                );
1196                map
1197            },
1198        };
1199        assert_eq!(result, report);
1200    }
1201
1202    #[test]
1203    fn test_report_generation_with_object_destructuring() {
1204        let js_source = indoc! { r#"
1205        const { a, b } = someObject;
1206        const { c: renamed } = anotherObject;
1207
1208        function processValues({ x, y }) {
1209            return x + y;
1210        }
1211
1212        const result = processValues({ x: a, y: b });
1213        "#};
1214        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1215        insta::with_settings!({
1216            description => js_source,
1217            omit_expression => true
1218        }, {
1219            insta::assert_yaml_snapshot!(context_stack_references);
1220        });
1221
1222        let result = build_report(&context_stack_references);
1223        insta::with_settings!({
1224            sort_maps => true,
1225            description => js_source,
1226            omit_expression => true
1227        }, {
1228            insta::assert_yaml_snapshot!(result);
1229        });
1230    }
1231}
1232
1233#[cfg(test)]
1234mod destructuring_tests {
1235    use super::*;
1236    use indoc::indoc;
1237
1238    #[test]
1239    fn test_basic_object_destructuring() {
1240        let js_source = indoc! { r#"
1241            const { a, b } = { a: 1, b: 2 };
1242        "#};
1243        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1244        insta::with_settings!({
1245            sort_maps => true,
1246            description => js_source,
1247            omit_expression => true
1248        }, {
1249            insta::assert_yaml_snapshot!(context_stack_references);
1250        });
1251
1252        let result = build_report(&context_stack_references);
1253        insta::with_settings!({
1254            sort_maps => true,
1255            description => js_source,
1256            omit_expression => true
1257        }, {
1258            insta::assert_yaml_snapshot!(result);
1259        });
1260
1261        result;
1262    }
1263
1264    #[test]
1265    fn test_object_destructuring_with_renaming_and_default() {
1266        let js_source = indoc! { r#"
1267            const { c: renamed, d = 'default' } = { c: 3 };
1268        "#};
1269        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1270        insta::with_settings!({
1271            sort_maps => true,
1272            description => js_source,
1273            omit_expression => true
1274        }, {
1275            insta::assert_yaml_snapshot!(context_stack_references);
1276        });
1277
1278        let result = build_report(&context_stack_references);
1279        insta::with_settings!({
1280            sort_maps => true,
1281            description => js_source,
1282            omit_expression => true
1283        }, {
1284            insta::assert_yaml_snapshot!(result);
1285        });
1286
1287        result;
1288    }
1289
1290    #[test]
1291    fn test_nested_object_destructuring() {
1292        let js_source = indoc! { r#"
1293            const { e: { f } } = { e: { f: 4 } };
1294        "#};
1295        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1296        insta::with_settings!({
1297            sort_maps => true,
1298            description => js_source,
1299            omit_expression => true
1300        }, {
1301            insta::assert_yaml_snapshot!(context_stack_references);
1302        });
1303
1304        let result = build_report(&context_stack_references);
1305        insta::with_settings!({
1306            sort_maps => true,
1307            description => js_source,
1308            omit_expression => true
1309        }, {
1310            insta::assert_yaml_snapshot!(result);
1311        });
1312
1313        result;
1314    }
1315
1316    #[test]
1317    fn test_basic_array_destructuring() {
1318        let js_source = indoc! { r#"
1319            const [g, h] = [5, 6];
1320        "#};
1321        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1322        insta::with_settings!({
1323            sort_maps => true,
1324            description => js_source,
1325            omit_expression => true
1326        }, {
1327            insta::assert_yaml_snapshot!(context_stack_references);
1328        });
1329
1330        let result = build_report(&context_stack_references);
1331        insta::with_settings!({
1332            sort_maps => true,
1333            description => js_source,
1334            omit_expression => true
1335        }, {
1336            insta::assert_yaml_snapshot!(result);
1337        });
1338
1339        result;
1340    }
1341
1342    #[test]
1343    fn test_array_destructuring_with_gaps() {
1344        let js_source = indoc! { r#"
1345            const [i, , j] = [7, 8, 9];
1346        "#};
1347        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1348        insta::with_settings!({
1349            sort_maps => true,
1350            description => js_source,
1351            omit_expression => true
1352        }, {
1353            insta::assert_yaml_snapshot!(context_stack_references);
1354        });
1355
1356        let result = build_report(&context_stack_references);
1357        insta::with_settings!({
1358            sort_maps => true,
1359            description => js_source,
1360            omit_expression => true
1361        }, {
1362            insta::assert_yaml_snapshot!(result);
1363        });
1364
1365        result;
1366    }
1367
1368    #[test]
1369    fn test_array_destructuring_with_rest() {
1370        let js_source = indoc! { r#"
1371            const [k, ...rest] = [10, 11, 12];
1372        "#};
1373        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1374        insta::with_settings!({
1375            sort_maps => true,
1376            description => js_source,
1377            omit_expression => true
1378        }, {
1379            insta::assert_yaml_snapshot!(context_stack_references);
1380        });
1381
1382        let result = build_report(&context_stack_references);
1383        insta::with_settings!({
1384            sort_maps => true,
1385            description => js_source,
1386            omit_expression => true
1387        }, {
1388            insta::assert_yaml_snapshot!(result);
1389        });
1390
1391        result;
1392    }
1393
1394    #[test]
1395    fn test_mixed_destructuring() {
1396        let js_source = indoc! { r#"
1397            const { l, m: [n, o] } = { l: 13, m: [14, 15] };
1398        "#};
1399        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1400        insta::with_settings!({
1401            sort_maps => true,
1402            description => js_source,
1403            omit_expression => true
1404        }, {
1405            insta::assert_yaml_snapshot!(context_stack_references);
1406        });
1407
1408        let result = build_report(&context_stack_references);
1409        insta::with_settings!({
1410            sort_maps => true,
1411            description => js_source,
1412            omit_expression => true
1413        }, {
1414            insta::assert_yaml_snapshot!(result);
1415        });
1416
1417        result;
1418    }
1419
1420    #[test]
1421    fn test_destructuring_in_function_parameters() {
1422        let js_source = indoc! { r#"
1423            function printPerson({ name, age = 30 }) {
1424                console.log(`${name} is ${age} years old`);
1425            }
1426        "#};
1427        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1428        insta::with_settings!({
1429            sort_maps => true,
1430            description => js_source,
1431            omit_expression => true
1432        }, {
1433            insta::assert_yaml_snapshot!(context_stack_references);
1434        });
1435
1436        let result = build_report(&context_stack_references);
1437        insta::with_settings!({
1438            sort_maps => true,
1439            description => js_source,
1440            omit_expression => true
1441        }, {
1442            insta::assert_yaml_snapshot!(result);
1443        });
1444
1445        result;
1446    }
1447
1448    #[test]
1449    fn test_destructuring_with_computed_property_names() {
1450        let js_source = indoc! { r#"
1451            const key = 'p';
1452            const { [key]: value } = { p: 16 };
1453        "#};
1454        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1455        insta::with_settings!({
1456            sort_maps => true,
1457            description => js_source,
1458            omit_expression => true
1459        }, {
1460            insta::assert_yaml_snapshot!(context_stack_references);
1461        });
1462
1463        let result = build_report(&context_stack_references);
1464        insta::with_settings!({
1465            sort_maps => true,
1466            description => js_source,
1467            omit_expression => true
1468        }, {
1469            insta::assert_yaml_snapshot!(result);
1470        });
1471
1472        result;
1473    }
1474
1475    #[test]
1476    fn test_complex_nested_destructuring() {
1477        let js_source = indoc! { r#"
1478            const { q: { r: [s, { t }] } } = { q: { r: [17, { t: 18 }] } };
1479        "#};
1480        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1481        insta::with_settings!({
1482            sort_maps => true,
1483            description => js_source,
1484            omit_expression => true
1485        }, {
1486            insta::assert_yaml_snapshot!(context_stack_references);
1487        });
1488
1489        let result = build_report(&context_stack_references);
1490        insta::with_settings!({
1491            sort_maps => true,
1492            description => js_source,
1493            omit_expression => true
1494        }, {
1495            insta::assert_yaml_snapshot!(result);
1496        });
1497
1498        result;
1499    }
1500
1501    #[test]
1502    fn test_object_destructuring_with_rest_properties() {
1503        let js_source = indoc! { r#"
1504            const { u, ...others } = { u: 19, v: 20, w: 21 };
1505        "#};
1506        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1507        insta::with_settings!({
1508            sort_maps => true,
1509            description => js_source,
1510            omit_expression => true
1511        }, {
1512            insta::assert_yaml_snapshot!(context_stack_references);
1513        });
1514
1515        let result = build_report(&context_stack_references);
1516        insta::with_settings!({
1517            sort_maps => true,
1518            description => js_source,
1519            omit_expression => true
1520        }, {
1521            insta::assert_yaml_snapshot!(result);
1522        });
1523
1524        result;
1525    }
1526
1527    #[test]
1528    fn test_destructuring_in_for_of_loops() {
1529        let js_source = indoc! { r#"
1530            const items = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
1531            for (const { id, name } of items) {
1532                console.log(`${id}: ${name}`);
1533            }
1534        "#};
1535        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1536        insta::with_settings!({
1537            sort_maps => true,
1538            description => js_source,
1539            omit_expression => true
1540        }, {
1541            insta::assert_yaml_snapshot!(context_stack_references);
1542        });
1543
1544        let result = build_report(&context_stack_references);
1545        insta::with_settings!({
1546            sort_maps => true,
1547            description => js_source,
1548            omit_expression => true
1549        }, {
1550            insta::assert_yaml_snapshot!(result);
1551        });
1552
1553        result;
1554    }
1555
1556    #[test]
1557    fn test_destructuring_with_alias_and_default_value() {
1558        let js_source = indoc! { r#"
1559            const { x: newX = 100 } = {};
1560        "#};
1561        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1562        insta::with_settings!({
1563            sort_maps => true,
1564            description => js_source,
1565            omit_expression => true
1566        }, {
1567            insta::assert_yaml_snapshot!(context_stack_references);
1568        });
1569
1570        let result = build_report(&context_stack_references);
1571        insta::with_settings!({
1572            sort_maps => true,
1573            description => js_source,
1574            omit_expression => true
1575        }, {
1576            insta::assert_yaml_snapshot!(result);
1577        });
1578
1579        result;
1580    }
1581
1582    #[test]
1583    fn test_array_destructuring_with_default_values() {
1584        let js_source = indoc! { r#"
1585            const [y = 200, z = 300] = [22];
1586        "#};
1587        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1588        insta::with_settings!({
1589            sort_maps => true,
1590            description => js_source,
1591            omit_expression => true
1592        }, {
1593            insta::assert_yaml_snapshot!(context_stack_references);
1594        });
1595
1596        let result = build_report(&context_stack_references);
1597        insta::with_settings!({
1598            sort_maps => true,
1599            description => js_source,
1600            omit_expression => true
1601        }, {
1602            insta::assert_yaml_snapshot!(result);
1603        });
1604
1605        result;
1606    }
1607
1608    #[test]
1609    fn test_destructuring_returned_arrays() {
1610        let js_source = indoc! { r#"
1611            function returnArray() {
1612                return [1, 2, 3];
1613            }
1614            const [first, ...restArray] = returnArray();
1615        "#};
1616        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1617        insta::with_settings!({
1618            sort_maps => true,
1619            description => js_source,
1620            omit_expression => true
1621        }, {
1622            insta::assert_yaml_snapshot!(context_stack_references);
1623        });
1624
1625        let result = build_report(&context_stack_references);
1626        insta::with_settings!({
1627            sort_maps => true,
1628            description => js_source,
1629            omit_expression => true
1630        }, {
1631            insta::assert_yaml_snapshot!(result);
1632        });
1633
1634        result;
1635    }
1636
1637    #[test]
1638    fn test_variable_swapping_with_destructuring() {
1639        let js_source = indoc! { r#"
1640            let aa = 'first', bb = 'second';
1641            [aa, bb] = [bb, aa];
1642        "#};
1643        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1644        insta::with_settings!({
1645            sort_maps => true,
1646            description => js_source,
1647            omit_expression => true
1648        }, {
1649            insta::assert_yaml_snapshot!(context_stack_references);
1650        });
1651
1652        let result = build_report(&context_stack_references);
1653        insta::with_settings!({
1654            sort_maps => true,
1655            description => js_source,
1656            omit_expression => true
1657        }, {
1658            insta::assert_yaml_snapshot!(result);
1659        });
1660
1661        result;
1662    }
1663}
1664
1665#[cfg(test)]
1666mod for_loop_tests {
1667    use super::*;
1668    use indoc::indoc;
1669
1670    #[test]
1671    fn test_traditional_for_loop() {
1672        let js_source = indoc! { r#"
1673            for (let i = 0; i < 5; i++) {
1674                console.log(i);
1675            }
1676        "#};
1677        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1678        insta::with_settings!({
1679            sort_maps => true,
1680            description => js_source,
1681            omit_expression => true
1682        }, {
1683            insta::assert_yaml_snapshot!(context_stack_references);
1684        });
1685
1686        let result = build_report(&context_stack_references);
1687        insta::with_settings!({
1688            sort_maps => true,
1689            description => js_source,
1690            omit_expression => true
1691        }, {
1692            insta::assert_yaml_snapshot!(result);
1693        });
1694
1695        result;
1696    }
1697
1698    #[test]
1699    fn test_for_of_loop_with_array() {
1700        let js_source = indoc! { r#"
1701            const array = [1, 2, 3, 4, 5];
1702            for (const item of array) {
1703                console.log(item);
1704            }
1705        "#};
1706        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1707        insta::with_settings!({
1708            sort_maps => true,
1709            description => js_source,
1710            omit_expression => true
1711        }, {
1712            insta::assert_yaml_snapshot!(context_stack_references);
1713        });
1714
1715        let result = build_report(&context_stack_references);
1716        insta::with_settings!({
1717            sort_maps => true,
1718            description => js_source,
1719            omit_expression => true
1720        }, {
1721            insta::assert_yaml_snapshot!(result);
1722        });
1723
1724        result;
1725    }
1726
1727    #[test]
1728    fn test_for_in_loop_with_object() {
1729        let js_source = indoc! { r#"
1730            const obj = { a: 1, b: 2, c: 3 };
1731            for (const key in obj) {
1732                console.log(key, obj[key]);
1733            }
1734        "#};
1735        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1736        insta::with_settings!({
1737            sort_maps => true,
1738            description => js_source,
1739            omit_expression => true
1740        }, {
1741            insta::assert_yaml_snapshot!(context_stack_references);
1742        });
1743
1744        let result = build_report(&context_stack_references);
1745        insta::with_settings!({
1746            sort_maps => true,
1747            description => js_source,
1748            omit_expression => true
1749        }, {
1750            insta::assert_yaml_snapshot!(result);
1751        });
1752
1753        result;
1754    }
1755
1756    #[test]
1757    fn test_for_loop_with_multiple_declarations() {
1758        let js_source = indoc! { r#"
1759            for (let i = 0, j = 10; i < j; i++, j--) {
1760                console.log(i, j);
1761            }
1762        "#};
1763        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1764        insta::with_settings!({
1765            sort_maps => true,
1766            description => js_source,
1767            omit_expression => true
1768        }, {
1769            insta::assert_yaml_snapshot!(context_stack_references);
1770        });
1771
1772        let result = build_report(&context_stack_references);
1773        insta::with_settings!({
1774            sort_maps => true,
1775            description => js_source,
1776            omit_expression => true
1777        }, {
1778            insta::assert_yaml_snapshot!(result);
1779        });
1780
1781        result;
1782    }
1783
1784    #[test]
1785    fn test_for_loop_with_break_statement() {
1786        let js_source = indoc! { r#"
1787            for (let i = 0; i < 10; i++) {
1788                if (i === 5) break;
1789                console.log(i);
1790            }
1791        "#};
1792        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1793        insta::with_settings!({
1794            sort_maps => true,
1795            description => js_source,
1796            omit_expression => true
1797        }, {
1798            insta::assert_yaml_snapshot!(context_stack_references);
1799        });
1800
1801        let result = build_report(&context_stack_references);
1802        insta::with_settings!({
1803            sort_maps => true,
1804            description => js_source,
1805            omit_expression => true
1806        }, {
1807            insta::assert_yaml_snapshot!(result);
1808        });
1809
1810        result;
1811    }
1812
1813    #[test]
1814    fn test_for_loop_with_continue_statement() {
1815        let js_source = indoc! { r#"
1816            for (let i = 0; i < 10; i++) {
1817                if (i % 2 === 0) continue;
1818                console.log(i);
1819            }
1820        "#};
1821        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1822        insta::with_settings!({
1823            sort_maps => true,
1824            description => js_source,
1825            omit_expression => true
1826        }, {
1827            insta::assert_yaml_snapshot!(context_stack_references);
1828        });
1829
1830        let result = build_report(&context_stack_references);
1831        insta::with_settings!({
1832            sort_maps => true,
1833            description => js_source,
1834            omit_expression => true
1835        }, {
1836            insta::assert_yaml_snapshot!(result);
1837        });
1838
1839        result;
1840    }
1841
1842    #[test]
1843    fn test_nested_for_loops() {
1844        let js_source = indoc! { r#"
1845            for (let i = 0; i < 3; i++) {
1846                for (let j = 0; j < 3; j++) {
1847                    console.log(i, j);
1848                }
1849            }
1850        "#};
1851        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1852        insta::with_settings!({
1853            sort_maps => true,
1854            description => js_source,
1855            omit_expression => true
1856        }, {
1857            insta::assert_yaml_snapshot!(context_stack_references);
1858        });
1859
1860        let result = build_report(&context_stack_references);
1861        insta::with_settings!({
1862            sort_maps => true,
1863            description => js_source,
1864            omit_expression => true
1865        }, {
1866            insta::assert_yaml_snapshot!(result);
1867        });
1868
1869        result;
1870    }
1871
1872    #[test]
1873    fn test_for_loop_with_let_declaration() {
1874        let js_source = indoc! { r#"
1875            for (let i = 0; i < 5; i++) {
1876                let x = i * 2;
1877                console.log(x);
1878            }
1879        "#};
1880        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1881        insta::with_settings!({
1882            sort_maps => true,
1883            description => js_source,
1884            omit_expression => true
1885        }, {
1886            insta::assert_yaml_snapshot!(context_stack_references);
1887        });
1888
1889        let result = build_report(&context_stack_references);
1890        insta::with_settings!({
1891            sort_maps => true,
1892            description => js_source,
1893            omit_expression => true
1894        }, {
1895            insta::assert_yaml_snapshot!(result);
1896        });
1897
1898        result;
1899    }
1900
1901    #[test]
1902    fn test_for_loop_with_const_declaration() {
1903        let js_source = indoc! { r#"
1904            for (let i = 0; i < 5; i++) {
1905                const y = i * 2;
1906                console.log(y);
1907            }
1908        "#};
1909        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1910        insta::with_settings!({
1911            sort_maps => true,
1912            description => js_source,
1913            omit_expression => true
1914        }, {
1915            insta::assert_yaml_snapshot!(context_stack_references);
1916        });
1917
1918        let result = build_report(&context_stack_references);
1919        insta::with_settings!({
1920            sort_maps => true,
1921            description => js_source,
1922            omit_expression => true
1923        }, {
1924            insta::assert_yaml_snapshot!(result);
1925        });
1926
1927        result;
1928    }
1929
1930    #[test]
1931    fn test_for_loop_with_var_declaration() {
1932        let js_source = indoc! { r#"
1933            for (var i = 0; i < 5; i++) {
1934                var z = i * 2;
1935                console.log(z);
1936            }
1937        "#};
1938        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1939        insta::with_settings!({
1940            sort_maps => true,
1941            description => js_source,
1942            omit_expression => true
1943        }, {
1944            insta::assert_yaml_snapshot!(context_stack_references);
1945        });
1946
1947        let result = build_report(&context_stack_references);
1948        insta::with_settings!({
1949            sort_maps => true,
1950            description => js_source,
1951            omit_expression => true
1952        }, {
1953            insta::assert_yaml_snapshot!(result);
1954        });
1955
1956        result;
1957    }
1958
1959    #[test]
1960    fn test_for_of_loop_with_destructuring() {
1961        let js_source = indoc! { r#"
1962            const pairs = [[1, 'one'], [2, 'two'], [3, 'three']];
1963            for (const [num, word] of pairs) {
1964                console.log(num, word);
1965            }
1966        "#};
1967        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1968        insta::with_settings!({
1969            sort_maps => true,
1970            description => js_source,
1971            omit_expression => true
1972        }, {
1973            insta::assert_yaml_snapshot!(context_stack_references);
1974        });
1975
1976        let result = build_report(&context_stack_references);
1977        insta::with_settings!({
1978            sort_maps => true,
1979            description => js_source,
1980            omit_expression => true
1981        }, {
1982            insta::assert_yaml_snapshot!(result);
1983        });
1984
1985        result;
1986    }
1987
1988    #[test]
1989    fn test_for_of_loop_with_object_destructuring() {
1990        let js_source = indoc! { r#"
1991            const pairs = [{a: 1, b:2}];
1992            for (const {a, b} of pairs) {
1993                console.log(a, b);
1994            }
1995        "#};
1996        let context_stack_references = extract_dependencies_js(js_source).unwrap();
1997        insta::with_settings!({
1998            sort_maps => true,
1999            description => js_source,
2000            omit_expression => true
2001        }, {
2002            insta::assert_yaml_snapshot!(context_stack_references);
2003        });
2004
2005        let result = build_report(&context_stack_references);
2006        insta::with_settings!({
2007            sort_maps => true,
2008            description => js_source,
2009            omit_expression => true
2010        }, {
2011            insta::assert_yaml_snapshot!(result);
2012        });
2013
2014        result;
2015    }
2016}
2017
2018
2019
2020#[cfg(test)]
2021mod anonymous_function_tests {
2022    use super::*;
2023    use indoc::indoc;
2024
2025    #[test]
2026    fn test_basic_anonymous_function() {
2027        let js_source = indoc! { r#"
2028            const func = function(x, y) {
2029                return x + y;
2030            };
2031            func(1, 2);
2032        "#};
2033        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2034        let result = build_report(&context_stack_references);
2035
2036        assert!(result.cell_depended_values.is_empty(), "Anonymous function parameters should not be depended upon");
2037        assert!(result.cell_exposed_values.contains_key("func"), "The function should be exposed");
2038    }
2039
2040    #[test]
2041    fn test_arrow_function() {
2042        let js_source = indoc! { r#"
2043            const arrowFunc = (a, b) => a * b;
2044            arrowFunc(3, 4);
2045        "#};
2046        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2047        let result = build_report(&context_stack_references);
2048
2049        assert!(result.cell_depended_values.is_empty(), "Arrow function parameters should not be depended upon");
2050        assert!(result.cell_exposed_values.contains_key("arrowFunc"), "The arrow function should be exposed");
2051    }
2052
2053    #[test]
2054    fn test_iife() {
2055        let js_source = indoc! { r#"
2056            (function(x) {
2057                console.log(x);
2058            })(5);
2059        "#};
2060        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2061        let result = build_report(&context_stack_references);
2062
2063        assert!(result.cell_depended_values.is_empty(), "IIFE parameters should not be depended upon");
2064    }
2065
2066    #[test]
2067    fn test_function_as_argument() {
2068        let js_source = indoc! { r#"
2069            function higherOrder(callback) {
2070                callback(10);
2071            }
2072            higherOrder(function(num) {
2073                return num * 2;
2074            });
2075        "#};
2076        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2077        let result = build_report(&context_stack_references);
2078
2079        assert!(!result.cell_depended_values.contains_key("num"), "Callback parameter should not be depended upon");
2080        assert!(result.triggerable_functions.contains_key("higherOrder"), "The higher-order function should be exposed");
2081    }
2082
2083    #[test]
2084    fn test_nested_anonymous_functions() {
2085        let js_source = indoc! { r#"
2086            const nestedFunc = function(x) {
2087                return function(y) {
2088                    return x + y;
2089                };
2090            };
2091            nestedFunc(5)(3);
2092        "#};
2093        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2094        let result = build_report(&context_stack_references);
2095
2096        assert!(result.cell_depended_values.is_empty(), "Nested anonymous function parameters should not be depended upon");
2097        assert!(result.cell_exposed_values.contains_key("nestedFunc"), "The outer function should be exposed");
2098    }
2099
2100    #[test]
2101    fn test_anonymous_function_with_destructuring() {
2102        let js_source = indoc! { r#"
2103            const destructuringFunc = function({ a, b }) {
2104                return a + b;
2105            };
2106            destructuringFunc({ a: 1, b: 2 });
2107        "#};
2108        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2109        let result = build_report(&context_stack_references);
2110
2111        assert!(result.cell_depended_values.is_empty(), "Destructured parameters should not be depended upon");
2112        assert!(result.cell_exposed_values.contains_key("destructuringFunc"), "The function should be exposed");
2113    }
2114
2115    #[test]
2116    fn test_anonymous_function_in_object() {
2117        let js_source = indoc! { r#"
2118            const obj = {
2119                method: function(x) {
2120                    return x * x;
2121                }
2122            };
2123            obj.method(4);
2124        "#};
2125        let context_stack_references = extract_dependencies_js(js_source).unwrap();
2126        let result = build_report(&context_stack_references);
2127
2128        assert!(result.cell_depended_values.is_empty(), "Object method parameters should not be depended upon");
2129        assert!(result.cell_exposed_values.contains_key("obj"), "The object should be exposed");
2130    }
2131}