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