Skip to main content

turbopack_ecmascript/analyzer/
mod.rs

1use swc_core::{
2    common::Mark,
3    ecma::ast::{Id, Ident},
4};
5
6pub(crate) use self::imports::ImportMap;
7
8pub mod builtin;
9pub mod bump_vec;
10pub(crate) mod cjs_ast;
11pub mod graph;
12pub mod imports;
13pub mod linker;
14pub mod side_effects;
15pub mod top_level_await;
16pub mod well_known;
17
18mod jsvalue;
19pub use bump_vec::BumpVec;
20pub use bumpalo::Bump;
21pub use jsvalue::*;
22pub use thread_local::ThreadLocal;
23pub use well_known::{kinds::*, require_context::*};
24
25fn is_unresolved(i: &Ident, unresolved_mark: Mark) -> bool {
26    i.ctxt.outer() == unresolved_mark
27}
28
29fn is_unresolved_id(i: &Id, unresolved_mark: Mark) -> bool {
30    i.1.outer() == unresolved_mark
31}
32
33/// Whether a visitor — or one of the builtin / well-known rewrite helpers —
34/// changed the `JsValue` it was given. Returned alongside the (possibly
35/// rewritten) value. [`Modified::Yes`] makes the linker re-enter the value for
36/// further processing; [`Modified::No`] means it is final.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Modified {
39    Yes,
40    No,
41}
42
43impl Modified {
44    /// `true` if the value was modified.
45    pub fn is_modified(self) -> bool {
46        matches!(self, Modified::Yes)
47    }
48}
49
50impl From<bool> for Modified {
51    fn from(modified: bool) -> Self {
52        if modified {
53            Modified::Yes
54        } else {
55            Modified::No
56        }
57    }
58}
59
60#[doc(hidden)]
61pub mod test_utils {
62    use anyhow::Result;
63    use turbo_rcstr::rcstr;
64    use turbo_tasks::{FxIndexMap, PrettyPrintError, Vc};
65    use turbopack_core::compile_time_info::CompileTimeInfo;
66
67    use super::{
68        ConstantValue, JsValue, JsValueUrlKind, Modified, ModuleValue, WellKnownFunctionKind,
69        WellKnownObjectKind, builtin::early_replace_builtin, well_known::replace_well_known,
70    };
71    use crate::{
72        analyzer::{
73            Bump, RequireContextValue, ThreadLocal, builtin::replace_builtin,
74            imports::ImportAttributes, parse_require_context,
75        },
76        utils::module_value_to_well_known_object,
77    };
78
79    pub async fn early_visitor<'a>(
80        _arena: &'a ThreadLocal<Bump>,
81        mut v: JsValue<'a>,
82    ) -> Result<(JsValue<'a>, Modified)> {
83        let m = early_replace_builtin(&mut v);
84        Ok((v, m))
85    }
86
87    /// Visitor that replaces well known functions and objects with their
88    /// corresponding values. Returns the new value and whether it was modified.
89    pub async fn visitor<'a>(
90        arena: &'a ThreadLocal<Bump>,
91        v: JsValue<'a>,
92        compile_time_info: Vc<CompileTimeInfo>,
93        attributes: &ImportAttributes,
94    ) -> Result<(JsValue<'a>, Modified)> {
95        let ImportAttributes { ignore, .. } = *attributes;
96        let mut new_value = match v {
97            JsValue::Call(_, ref call)
98                if matches!(
99                    call.callee(),
100                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import)
101                ) =>
102            {
103                match &call.args()[0] {
104                    JsValue::Constant(ConstantValue::Str(v)) => JsValue::promise(
105                        arena.get_or_default(),
106                        JsValue::Module(ModuleValue {
107                            module: v.as_atom().into_owned().into(),
108                            annotations: None,
109                            analyze_for_constants: false,
110                            reference: None,
111                        }),
112                    ),
113                    _ => v.into_unknown(true, rcstr!("import() non constant")),
114                }
115            }
116            JsValue::Call(_, ref call)
117                if matches!(
118                    call.callee(),
119                    JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire)
120                ) =>
121            {
122                if let [JsValue::Member(_, obj, prop)] = call.args()
123                    && matches!(
124                        &**obj,
125                        JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta)
126                    )
127                    && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop
128                    && prop.as_str() == "url"
129                {
130                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require)
131                } else {
132                    v.into_unknown(true, rcstr!("createRequire() non constant"))
133                }
134            }
135            JsValue::Call(_, ref call)
136                if matches!(
137                    call.callee(),
138                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve)
139                ) =>
140            {
141                match &call.args()[0] {
142                    JsValue::Constant(v) => (v.to_string() + "/resolved/lib/index.js").into(),
143                    _ => v.into_unknown(true, rcstr!("require.resolve non constant")),
144                }
145            }
146            JsValue::Call(_, ref call)
147                if matches!(
148                    call.callee(),
149                    JsValue::WellKnownFunction(WellKnownFunctionKind::ImportMetaGlob)
150                ) =>
151            {
152                v.into_unknown(false, rcstr!("import.meta.glob()"))
153            }
154            JsValue::Call(_, ref call)
155                if matches!(
156                    call.callee(),
157                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext)
158                ) =>
159            {
160                match parse_require_context(call.args()) {
161                    Ok(options) => {
162                        let mut map = FxIndexMap::default();
163
164                        map.insert(
165                            rcstr!("./a"),
166                            format!("[context: {}]/a", options.dir).into(),
167                        );
168                        map.insert(
169                            rcstr!("./b"),
170                            format!("[context: {}]/b", options.dir).into(),
171                        );
172                        map.insert(
173                            rcstr!("./c"),
174                            format!("[context: {}]/c", options.dir).into(),
175                        );
176
177                        JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(
178                            Box::new(RequireContextValue(map)),
179                        ))
180                    }
181                    Err(err) => v.into_unknown(true, PrettyPrintError(&err).to_string().into()),
182                }
183            }
184            JsValue::New(_, ref call)
185                if matches!(
186                    call.callee(),
187                    JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor)
188                ) =>
189            {
190                if let [
191                    JsValue::Constant(ConstantValue::Str(url)),
192                    JsValue::Member(_, obj, prop),
193                ] = call.args()
194                    && matches!(
195                        &**obj,
196                        JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta)
197                    )
198                    && let JsValue::Constant(ConstantValue::Str(prop)) = &**prop
199                {
200                    if prop.as_str() == "url" {
201                        // TODO avoid clone
202                        JsValue::Url(url.clone(), JsValueUrlKind::Relative)
203                    } else {
204                        v.into_unknown(true, rcstr!("new non constant"))
205                    }
206                } else {
207                    v.into_unknown(true, rcstr!("new non constant"))
208                }
209            }
210            JsValue::FreeVar(ref var) => match &**var {
211                "__dirname" => rcstr!("__dirname").into(),
212                "__filename" => rcstr!("__filename").into(),
213
214                "require" => JsValue::unknown_if(
215                    ignore,
216                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
217                    true,
218                    rcstr!("ignored require"),
219                ),
220                "import" => JsValue::unknown_if(
221                    ignore,
222                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
223                    true,
224                    rcstr!("ignored import"),
225                ),
226                "Worker" => JsValue::unknown_if(
227                    ignore,
228                    JsValue::WellKnownFunction(WellKnownFunctionKind::WorkerConstructor),
229                    true,
230                    rcstr!("ignored Worker constructor"),
231                ),
232                "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define),
233                "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor),
234                "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule),
235                "Object" => JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject),
236                "Buffer" => JsValue::WellKnownObject(WellKnownObjectKind::NodeBuffer),
237                _ => v.into_unknown(true, rcstr!("unknown global")),
238            },
239            JsValue::Module(ref mv) => {
240                if let Some(wko) = module_value_to_well_known_object(mv) {
241                    wko
242                } else {
243                    return Ok((v, Modified::No));
244                }
245            }
246            _ => {
247                let (mut v, m1) = replace_well_known(arena, v, compile_time_info, true).await?;
248                let m2 = replace_builtin(arena.get_or_default(), &mut v);
249                let m = if m1.is_modified() || m2.is_modified() {
250                    Modified::Yes
251                } else {
252                    Modified::from(v.make_nested_operations_unknown())
253                };
254                return Ok((v, m));
255            }
256        };
257        new_value.normalize_shallow(arena.get_or_default());
258        Ok((new_value, Modified::Yes))
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::{mem::take, path::PathBuf, sync::Arc, time::Instant};
265
266    use bumpalo::boxed::Box as BumpBox;
267    use parking_lot::Mutex;
268    use rustc_hash::FxHashMap;
269    use swc_core::{
270        common::{
271            FileName, FilePathMapping, GLOBALS, Globals, Mark, SourceMap,
272            comments::SingleThreadedComments,
273        },
274        ecma::{
275            ast::{EsVersion, Id},
276            parser::parse_file_as_program,
277            transforms::base::resolver,
278            visit::VisitMutWith,
279        },
280        testing::{NormalizedOutput, fixture},
281    };
282    use turbo_rcstr::{RcStr, rcstr};
283    use turbo_tasks::{ResolvedVc, TurboTasks, util::FormatDuration};
284    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
285    use turbopack_core::{
286        compile_time_info::CompileTimeInfo,
287        environment::{Environment, ExecutionEnvironment, NodeJsEnvironment, NodeJsVersion},
288        target::{Arch, CompileTarget, Endianness, Libc, Platform},
289    };
290
291    use super::{
292        BumpVec, JsValue,
293        graph::{ConditionalKind, Effect, EffectArg, EvalContext, VarGraph, create_graph},
294        linker::link,
295    };
296    use crate::{
297        AnalyzeMode, SpecifiedModuleType,
298        analyzer::{Bump, ThreadLocal, graph::AssignmentScopes, imports::ImportAttributes},
299    };
300
301    #[fixture("tests/analyzer/graph/**/input.js")]
302    fn fixture(input: PathBuf) {
303        let input = RcStr::from(input.to_str().unwrap());
304        let rt = tokio::runtime::Builder::new_multi_thread()
305            .worker_threads(2)
306            .enable_all()
307            .build()
308            .unwrap();
309        rt.block_on(async move {
310            let tt = TurboTasks::new(TurboTasksBackend::new(
311                BackendOptions::default(),
312                noop_backing_storage(),
313            ));
314            tt.run_once(async move {
315                fixture_op(input).read_strongly_consistent().await?;
316                anyhow::Ok(())
317            })
318            .await
319            .unwrap();
320        });
321    }
322
323    #[turbo_tasks::function(operation, root)]
324    async fn fixture_op(input: RcStr) -> anyhow::Result<()> {
325        let input = PathBuf::from(input.as_str());
326        let graph_snapshot_path = input.with_file_name("graph.snapshot");
327        let graph_explained_snapshot_path = input.with_file_name("graph-explained.snapshot");
328        let graph_effects_snapshot_path = input.with_file_name("graph-effects.snapshot");
329        let resolved_explained_snapshot_path = input.with_file_name("resolved-explained.snapshot");
330        let resolved_effects_snapshot_path = input.with_file_name("resolved-effects.snapshot");
331        let large_marker = input.with_file_name("large");
332
333        let cm: Arc<SourceMap> = Arc::new(SourceMap::new(FilePathMapping::empty()));
334        let globals = Arc::new(Globals::new());
335        let arena = ThreadLocal::new();
336
337        // Keep all non-`Send` SWC types (`SingleThreadedComments`, `Lrc<SourceFile>`)
338        // confined to this synchronous block so they don't have to cross an `.await`
339        // and break the `Send` bound on `tt.run_once`'s future.
340        let (eval_context, mut var_graph) = GLOBALS.set(&globals, || {
341            let fm = cm.load_file(&input).unwrap();
342            let comments = SingleThreadedComments::default();
343            let mut m = parse_file_as_program(
344                &fm,
345                Default::default(),
346                EsVersion::latest(),
347                Some(&comments),
348                &mut vec![],
349            )
350            .map_err(|err| anyhow::anyhow!("parse error: {err:?}"))?;
351
352            let unresolved_mark = Mark::new();
353            let top_level_mark = Mark::new();
354            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
355
356            let eval_context = EvalContext::new(
357                Some(&m),
358                unresolved_mark,
359                top_level_mark,
360                Default::default(),
361                Some(&comments),
362            );
363
364            let var_graph = create_graph(
365                arena.get_or_default(),
366                &m,
367                &eval_context,
368                AnalyzeMode::code_generation_and_tracing(),
369                true,
370                SpecifiedModuleType::EcmaScript,
371                true,
372                false,
373            );
374            anyhow::Ok((eval_context, var_graph))
375        })?;
376        let var_cache = Default::default();
377
378        let mut named_values = var_graph
379            .values
380            .iter()
381            .map(|((id, ctx), value)| {
382                let unique = var_graph.values.keys().filter(|(i, _)| id == i).count() == 1;
383                let value = value.clone_in(arena.get_or_default());
384                if unique {
385                    (id.to_string(), ((id.clone(), *ctx), value))
386                } else {
387                    (format!("{id}{ctx:?}"), ((id.clone(), *ctx), value))
388                }
389            })
390            .collect::<Vec<_>>();
391        named_values.sort_by(|a, b| a.0.cmp(&b.0));
392
393        fn explain_all<'x, 'a: 'x>(
394            values: impl IntoIterator<Item = (&'x String, &'x JsValue<'a>, Option<AssignmentScopes>)>,
395        ) -> String {
396            values
397                .into_iter()
398                .map(|(id, value, assignment_scopes)| {
399                    let non_root_assignments = match assignment_scopes {
400                        Some(AssignmentScopes::AllInModuleEvalScope) => " (const after eval)",
401                        _ => "",
402                    };
403                    let (explainer, hints) = value.explain(10, 5);
404                    format!("{id}{non_root_assignments} = {explainer}{hints}")
405                })
406                .collect::<Vec<_>>()
407                .join("\n\n")
408        }
409
410        {
411            // Dump snapshot of graph
412
413            let large = large_marker.exists();
414
415            if !large {
416                NormalizedOutput::from(format!(
417                    "{:#?}",
418                    named_values
419                        .iter()
420                        .map(|(name, (_, value))| (name, value))
421                        .collect::<Vec<_>>()
422                ))
423                .compare_to_file(&graph_snapshot_path)
424                .unwrap();
425            }
426            NormalizedOutput::from(explain_all(named_values.iter().map(
427                |(name, (id, value))| {
428                    (
429                        name,
430                        value,
431                        eval_context.imports.assignment_scopes.get(id).copied(),
432                    )
433                },
434            )))
435            .compare_to_file(&graph_explained_snapshot_path)
436            .unwrap();
437            if !large {
438                NormalizedOutput::from(format!("{:#?}", var_graph.effects))
439                    .compare_to_file(&graph_effects_snapshot_path)
440                    .unwrap();
441            }
442        }
443
444        {
445            // Dump snapshot of resolved
446
447            let start = Instant::now();
448            let mut resolved = Vec::new();
449            for (name, id) in named_values.iter().map(|(name, (id, _))| (name, id)) {
450                let start = Instant::now();
451                // Ideally this would use eval_context.imports.get_attributes(span), but the
452                // span isn't available here
453                let (res, steps) = resolve(
454                    &arena,
455                    &var_graph,
456                    JsValue::Variable(id.clone()),
457                    ImportAttributes::empty_ref(),
458                    &var_cache,
459                )
460                .await;
461                let time = start.elapsed();
462                if time.as_millis() > 1 {
463                    println!(
464                        "linking {} {name} took {} in {} steps",
465                        input.display(),
466                        FormatDuration(time),
467                        steps
468                    );
469                }
470
471                resolved.push((name.clone(), res));
472            }
473            let time = start.elapsed();
474            if time.as_millis() > 1 {
475                println!("linking {} took {}", input.display(), FormatDuration(time));
476            }
477
478            let start = Instant::now();
479            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
480            let time = start.elapsed();
481            if time.as_millis() > 1 {
482                println!(
483                    "explaining {} took {}",
484                    input.display(),
485                    FormatDuration(time)
486                );
487            }
488
489            NormalizedOutput::from(explainer)
490                .compare_to_file(&resolved_explained_snapshot_path)
491                .unwrap();
492        }
493
494        {
495            // Dump snapshot of resolved effects
496
497            let start = Instant::now();
498            let mut resolved = Vec::new();
499            let mut queue = take(&mut var_graph.effects)
500                .into_iter()
501                .map(|effect| (0, effect))
502                .rev()
503                .collect::<Vec<_>>();
504            let mut i = 0;
505            while let Some((parent, effect)) = queue.pop() {
506                i += 1;
507                let start = Instant::now();
508                async fn handle_args<'a>(
509                    arena: &'a ThreadLocal<Bump>,
510                    args: BumpVec<'a, EffectArg<'a>>,
511                    queue: &mut Vec<(usize, Effect<'a>)>,
512                    var_graph: &VarGraph<'a>,
513                    var_cache: &Mutex<FxHashMap<Id, JsValue<'a>>>,
514                    i: usize,
515                ) -> Vec<JsValue<'a>> {
516                    let mut new_args = Vec::with_capacity(args.len());
517                    for arg in args {
518                        match arg {
519                            EffectArg::Value(v) => {
520                                new_args.push(
521                                    resolve(
522                                        arena,
523                                        var_graph,
524                                        v,
525                                        ImportAttributes::empty_ref(),
526                                        var_cache,
527                                    )
528                                    .await
529                                    .0,
530                                );
531                            }
532                            EffectArg::Closure(v, effects) => {
533                                new_args.push(
534                                    resolve(
535                                        arena,
536                                        var_graph,
537                                        v,
538                                        ImportAttributes::empty_ref(),
539                                        var_cache,
540                                    )
541                                    .await
542                                    .0,
543                                );
544                                queue.extend(
545                                    BumpVec::from(BumpBox::into_inner(effects).effects)
546                                        .into_iter()
547                                        .rev()
548                                        .map(|e| (i, e)),
549                                );
550                            }
551                            EffectArg::Spread => {
552                                new_args.push(JsValue::unknown_empty(true, rcstr!("spread")));
553                            }
554                        }
555                    }
556                    new_args
557                }
558                let steps = match effect {
559                    Effect::Conditional {
560                        mut condition,
561                        kind,
562                        ..
563                    } => {
564                        let (condition, steps) = resolve(
565                            &arena,
566                            &var_graph,
567                            take(&mut *condition),
568                            ImportAttributes::empty_ref(),
569                            &var_cache,
570                        )
571                        .await;
572                        resolved.push((format!("{parent} -> {i} conditional"), condition));
573                        match BumpBox::into_inner(kind) {
574                            ConditionalKind::If { then } => {
575                                queue.extend(
576                                    BumpVec::from(then.effects)
577                                        .into_iter()
578                                        .rev()
579                                        .map(|e| (i, e)),
580                                );
581                            }
582                            ConditionalKind::Else { r#else } => {
583                                queue.extend(
584                                    BumpVec::from(r#else.effects)
585                                        .into_iter()
586                                        .rev()
587                                        .map(|e| (i, e)),
588                                );
589                            }
590                            ConditionalKind::IfElse { then, r#else }
591                            | ConditionalKind::Ternary { then, r#else } => {
592                                queue.extend(
593                                    BumpVec::from(r#else.effects)
594                                        .into_iter()
595                                        .rev()
596                                        .map(|e| (i, e)),
597                                );
598                                queue.extend(
599                                    BumpVec::from(then.effects)
600                                        .into_iter()
601                                        .rev()
602                                        .map(|e| (i, e)),
603                                );
604                            }
605                            ConditionalKind::IfElseMultiple { then, r#else } => {
606                                for then in BumpVec::from(then) {
607                                    queue.extend(
608                                        BumpVec::from(then.effects)
609                                            .into_iter()
610                                            .rev()
611                                            .map(|e| (i, e)),
612                                    );
613                                }
614                                for r#else in BumpVec::from(r#else) {
615                                    queue.extend(
616                                        BumpVec::from(r#else.effects)
617                                            .into_iter()
618                                            .rev()
619                                            .map(|e| (i, e)),
620                                    );
621                                }
622                            }
623                            ConditionalKind::And { expr }
624                            | ConditionalKind::Or { expr }
625                            | ConditionalKind::NullishCoalescing { expr }
626                            | ConditionalKind::Labeled { body: expr } => {
627                                queue.extend(
628                                    BumpVec::from(expr.effects)
629                                        .into_iter()
630                                        .rev()
631                                        .map(|e| (i, e)),
632                                );
633                            }
634                        };
635                        steps
636                    }
637                    Effect::Call {
638                        mut func,
639                        args,
640                        new,
641                        span,
642                        ..
643                    } => {
644                        let (func, steps) = resolve(
645                            &arena,
646                            &var_graph,
647                            take(&mut *func),
648                            eval_context.imports.get_attributes(span),
649                            &var_cache,
650                        )
651                        .await;
652                        let new_args =
653                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
654                        resolved.push((
655                            format!("{parent} -> {i} call"),
656                            if new {
657                                JsValue::new_from_iter(arena.get_or_default(), func, new_args)
658                            } else {
659                                JsValue::call_from_iter(arena.get_or_default(), func, new_args)
660                            },
661                        ));
662                        steps
663                    }
664                    Effect::FreeVar { var, .. } => {
665                        resolved.push((format!("{parent} -> {i} free var"), JsValue::FreeVar(var)));
666                        0
667                    }
668                    Effect::TypeOf { mut arg, .. } => {
669                        let (arg, steps) = resolve(
670                            &arena,
671                            &var_graph,
672                            take(&mut *arg),
673                            ImportAttributes::empty_ref(),
674                            &var_cache,
675                        )
676                        .await;
677                        resolved.push((
678                            format!("{parent} -> {i} typeof"),
679                            JsValue::type_of(arena.get_or_default(), arg),
680                        ));
681                        steps
682                    }
683                    Effect::MemberCall {
684                        mut obj,
685                        mut prop,
686                        args,
687                        ..
688                    } => {
689                        let (obj, obj_steps) = resolve(
690                            &arena,
691                            &var_graph,
692                            take(&mut *obj),
693                            ImportAttributes::empty_ref(),
694                            &var_cache,
695                        )
696                        .await;
697                        let (prop, prop_steps) = resolve(
698                            &arena,
699                            &var_graph,
700                            take(&mut *prop),
701                            ImportAttributes::empty_ref(),
702                            &var_cache,
703                        )
704                        .await;
705                        let new_args =
706                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
707                        resolved.push((
708                            format!("{parent} -> {i} member call"),
709                            JsValue::member_call_from_iter(
710                                arena.get_or_default(),
711                                obj,
712                                prop,
713                                new_args,
714                            ),
715                        ));
716                        obj_steps + prop_steps
717                    }
718                    Effect::DynamicImport { args, .. } => {
719                        let new_args =
720                            handle_args(&arena, args, &mut queue, &var_graph, &var_cache, i).await;
721                        resolved.push((
722                            format!("{parent} -> {i} dynamic import"),
723                            JsValue::call_from_iter(
724                                arena.get_or_default(),
725                                JsValue::FreeVar("import".into()),
726                                new_args,
727                            ),
728                        ));
729                        0
730                    }
731                    Effect::Unreachable { .. } => {
732                        resolved.push((
733                            format!("{parent} -> {i} unreachable"),
734                            JsValue::unknown_empty(true, rcstr!("unreachable")),
735                        ));
736                        0
737                    }
738                    Effect::ImportMeta { .. }
739                    | Effect::ImportedBinding { .. }
740                    | Effect::Member { .. }
741                    | Effect::DestructuredMember { .. }
742                    | Effect::In { .. } => 0,
743                };
744                let time = start.elapsed();
745                if time.as_millis() > 1 {
746                    println!(
747                        "linking effect {} took {} in {} steps",
748                        input.display(),
749                        FormatDuration(time),
750                        steps
751                    );
752                }
753            }
754            let time = start.elapsed();
755            if time.as_millis() > 1 {
756                println!(
757                    "linking effects {} took {}",
758                    input.display(),
759                    FormatDuration(time)
760                );
761            }
762
763            let start = Instant::now();
764            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
765            let time = start.elapsed();
766            if time.as_millis() > 1 {
767                println!(
768                    "explaining effects {} took {}",
769                    input.display(),
770                    FormatDuration(time)
771                );
772            }
773
774            NormalizedOutput::from(explainer)
775                .compare_to_file(&resolved_effects_snapshot_path)
776                .unwrap();
777        }
778
779        Ok(())
780    }
781
782    /// Builds the var graph for `src` (parsed as a sloppy script unless it uses module syntax)
783    /// and returns whether a direct `eval` was detected.
784    fn detect_direct_eval(src: &str) -> bool {
785        let cm: Arc<SourceMap> = Arc::new(SourceMap::new(FilePathMapping::empty()));
786        let globals = Globals::new();
787        GLOBALS.set(&globals, || {
788            let fm = cm.new_source_file(FileName::Anon.into(), src.to_string());
789            let mut m = parse_file_as_program(
790                &fm,
791                Default::default(),
792                EsVersion::latest(),
793                None,
794                &mut vec![],
795            )
796            .unwrap();
797            let unresolved_mark = Mark::new();
798            let top_level_mark = Mark::new();
799            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
800            let eval_context = EvalContext::new(
801                Some(&m),
802                unresolved_mark,
803                top_level_mark,
804                Default::default(),
805                None,
806            );
807            let arena = Bump::new();
808            create_graph(
809                &arena,
810                &m,
811                &eval_context,
812                AnalyzeMode::code_generation(),
813                true,
814                SpecifiedModuleType::Automatic,
815                false,
816                false,
817            )
818            .has_direct_eval
819        })
820    }
821
822    #[test]
823    fn direct_eval_detection() {
824        for src in [
825            "eval('x')",
826            "(eval)('x')",
827            "((eval))('x')",
828            "function f() { return eval('x') }",
829            "(() => { eval('x') })()",
830            "class C { m() { eval('x') } }",
831            "if (cond) { eval('x') }",
832            "export const v = eval('x')",
833        ] {
834            assert!(detect_direct_eval(src), "expected direct eval: {src}");
835        }
836
837        for src in [
838            "(0, eval)('x')",
839            "globalThis.eval('x')",
840            "window.eval('x')",
841            "eval?.('x')",
842            "const e = eval; e('x')",
843            "new eval('x')",
844            "function f(eval) { return eval('x') }",
845            "var eval = (x) => x; eval('x')",
846            "export const v = 1",
847        ] {
848            assert!(!detect_direct_eval(src), "expected no direct eval: {src}");
849        }
850    }
851
852    async fn resolve<'a>(
853        arena: &'a ThreadLocal<Bump>,
854        var_graph: &VarGraph<'a>,
855        val: JsValue<'a>,
856        attributes: &ImportAttributes,
857        var_cache: &Mutex<FxHashMap<Id, JsValue<'a>>>,
858    ) -> (JsValue<'a>, u32) {
859        // The caller (`fixture`) runs us inside `tt.run_once`, so a real
860        // turbo-tasks task context is already established here.
861        async {
862            let compile_time_info = CompileTimeInfo::builder(
863                Environment::new(ExecutionEnvironment::NodeJsLambda(
864                    NodeJsEnvironment {
865                        compile_target: CompileTarget {
866                            arch: Arch::X64,
867                            platform: Platform::Linux,
868                            endianness: Endianness::Little,
869                            libc: Libc::Glibc,
870                        }
871                        .resolved_cell(),
872                        node_version: NodeJsVersion::default().resolved_cell(),
873                        cwd: ResolvedVc::cell(None),
874                    }
875                    .resolved_cell(),
876                ))
877                .to_resolved()
878                .await?,
879            )
880            .cell()
881            .await?;
882            link(
883                arena,
884                var_graph,
885                val,
886                &(|val| Box::pin(super::test_utils::early_visitor(arena, val))),
887                &(|val| {
888                    Box::pin(super::test_utils::visitor(
889                        arena,
890                        val,
891                        compile_time_info,
892                        attributes,
893                    ))
894                }),
895                &Default::default(),
896                var_cache,
897            )
898            .await
899        }
900        .await
901        .unwrap()
902    }
903}