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 mod graph;
11pub mod imports;
12pub mod linker;
13pub mod side_effects;
14pub mod top_level_await;
15pub mod well_known;
16
17mod jsvalue;
18pub use jsvalue::*;
19pub use well_known::{kinds::*, require_context::*};
20
21fn is_unresolved(i: &Ident, unresolved_mark: Mark) -> bool {
22    i.ctxt.outer() == unresolved_mark
23}
24
25fn is_unresolved_id(i: &Id, unresolved_mark: Mark) -> bool {
26    i.1.outer() == unresolved_mark
27}
28
29#[doc(hidden)]
30pub mod test_utils {
31    use anyhow::Result;
32    use turbo_rcstr::rcstr;
33    use turbo_tasks::{FxIndexMap, PrettyPrintError, Vc};
34    use turbopack_core::compile_time_info::CompileTimeInfo;
35
36    use super::{
37        ConstantValue, JsValue, JsValueUrlKind, ModuleValue, WellKnownFunctionKind,
38        WellKnownObjectKind, builtin::early_replace_builtin, well_known::replace_well_known,
39    };
40    use crate::{
41        analyzer::{
42            RequireContextValue, builtin::replace_builtin, imports::ImportAttributes,
43            parse_require_context,
44        },
45        utils::module_value_to_well_known_object,
46    };
47
48    pub async fn early_visitor(mut v: JsValue) -> Result<(JsValue, bool)> {
49        let m = early_replace_builtin(&mut v);
50        Ok((v, m))
51    }
52
53    /// Visitor that replaces well known functions and objects with their
54    /// corresponding values. Returns the new value and whether it was modified.
55    pub async fn visitor(
56        v: JsValue,
57        compile_time_info: Vc<CompileTimeInfo>,
58        attributes: &ImportAttributes,
59    ) -> Result<(JsValue, bool)> {
60        let ImportAttributes { ignore, .. } = *attributes;
61        let mut new_value = match v {
62            JsValue::Call(_, ref call)
63                if matches!(
64                    call.callee(),
65                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import)
66                ) =>
67            {
68                match &call.args()[0] {
69                    JsValue::Constant(ConstantValue::Str(v)) => {
70                        JsValue::promise(JsValue::Module(ModuleValue {
71                            module: v.as_atom().into_owned().into(),
72                            annotations: None,
73                        }))
74                    }
75                    _ => v.into_unknown(true, rcstr!("import() non constant")),
76                }
77            }
78            JsValue::Call(_, ref call)
79                if matches!(
80                    call.callee(),
81                    JsValue::WellKnownFunction(WellKnownFunctionKind::CreateRequire)
82                ) =>
83            {
84                if let [
85                    JsValue::Member(
86                        _,
87                        box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta),
88                        box JsValue::Constant(ConstantValue::Str(prop)),
89                    ),
90                ] = call.args()
91                    && prop.as_str() == "url"
92                {
93                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require)
94                } else {
95                    v.into_unknown(true, rcstr!("createRequire() non constant"))
96                }
97            }
98            JsValue::Call(_, ref call)
99                if matches!(
100                    call.callee(),
101                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve)
102                ) =>
103            {
104                match &call.args()[0] {
105                    JsValue::Constant(v) => (v.to_string() + "/resolved/lib/index.js").into(),
106                    _ => v.into_unknown(true, rcstr!("require.resolve non constant")),
107                }
108            }
109            JsValue::Call(_, ref call)
110                if matches!(
111                    call.callee(),
112                    JsValue::WellKnownFunction(WellKnownFunctionKind::ImportMetaGlob)
113                ) =>
114            {
115                v.into_unknown(false, rcstr!("import.meta.glob()"))
116            }
117            JsValue::Call(_, ref call)
118                if matches!(
119                    call.callee(),
120                    JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext)
121                ) =>
122            {
123                match parse_require_context(call.args()) {
124                    Ok(options) => {
125                        let mut map = FxIndexMap::default();
126
127                        map.insert(
128                            rcstr!("./a"),
129                            format!("[context: {}]/a", options.dir).into(),
130                        );
131                        map.insert(
132                            rcstr!("./b"),
133                            format!("[context: {}]/b", options.dir).into(),
134                        );
135                        map.insert(
136                            rcstr!("./c"),
137                            format!("[context: {}]/c", options.dir).into(),
138                        );
139
140                        JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContextRequire(
141                            Box::new(RequireContextValue(map)),
142                        ))
143                    }
144                    Err(err) => v.into_unknown(true, PrettyPrintError(&err).to_string().into()),
145                }
146            }
147            JsValue::New(_, ref call)
148                if matches!(
149                    call.callee(),
150                    JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor)
151                ) =>
152            {
153                if let [
154                    JsValue::Constant(ConstantValue::Str(url)),
155                    JsValue::Member(
156                        _,
157                        box JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta),
158                        box JsValue::Constant(ConstantValue::Str(prop)),
159                    ),
160                ] = call.args()
161                {
162                    if prop.as_str() == "url" {
163                        // TODO avoid clone
164                        JsValue::Url(url.clone(), JsValueUrlKind::Relative)
165                    } else {
166                        v.into_unknown(true, rcstr!("new non constant"))
167                    }
168                } else {
169                    v.into_unknown(true, rcstr!("new non constant"))
170                }
171            }
172            JsValue::FreeVar(ref var) => match &**var {
173                "__dirname" => rcstr!("__dirname").into(),
174                "__filename" => rcstr!("__filename").into(),
175
176                "require" => JsValue::unknown_if(
177                    ignore,
178                    JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
179                    true,
180                    rcstr!("ignored require"),
181                ),
182                "import" => JsValue::unknown_if(
183                    ignore,
184                    JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
185                    true,
186                    rcstr!("ignored import"),
187                ),
188                "Worker" => JsValue::unknown_if(
189                    ignore,
190                    JsValue::WellKnownFunction(WellKnownFunctionKind::WorkerConstructor),
191                    true,
192                    rcstr!("ignored Worker constructor"),
193                ),
194                "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define),
195                "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor),
196                "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule),
197                "Object" => JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject),
198                "Buffer" => JsValue::WellKnownObject(WellKnownObjectKind::NodeBuffer),
199                _ => v.into_unknown(true, rcstr!("unknown global")),
200            },
201            JsValue::Module(ref mv) => {
202                if let Some(wko) = module_value_to_well_known_object(mv) {
203                    wko
204                } else {
205                    return Ok((v, false));
206                }
207            }
208            _ => {
209                let (mut v, m1) = replace_well_known(v, compile_time_info, true).await?;
210                let m2 = replace_builtin(&mut v);
211                let m = m1 || m2 || v.make_nested_operations_unknown();
212                return Ok((v, m));
213            }
214        };
215        new_value.normalize_shallow();
216        Ok((new_value, true))
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use std::{mem::take, path::PathBuf, sync::Arc, time::Instant};
223
224    use parking_lot::Mutex;
225    use rustc_hash::FxHashMap;
226    use swc_core::{
227        common::{
228            FilePathMapping, GLOBALS, Globals, Mark, SourceMap, comments::SingleThreadedComments,
229        },
230        ecma::{
231            ast::{EsVersion, Id},
232            parser::parse_file_as_program,
233            transforms::base::resolver,
234            visit::VisitMutWith,
235        },
236        testing::{NormalizedOutput, fixture},
237    };
238    use turbo_rcstr::{RcStr, rcstr};
239    use turbo_tasks::{ResolvedVc, TurboTasks, util::FormatDuration};
240    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
241    use turbopack_core::{
242        compile_time_info::CompileTimeInfo,
243        environment::{Environment, ExecutionEnvironment, NodeJsEnvironment, NodeJsVersion},
244        target::{Arch, CompileTarget, Endianness, Libc, Platform},
245    };
246
247    use super::{
248        JsValue,
249        graph::{ConditionalKind, Effect, EffectArg, EvalContext, VarGraph, create_graph},
250        linker::link,
251    };
252    use crate::{
253        AnalyzeMode,
254        analyzer::{graph::AssignmentScopes, imports::ImportAttributes},
255    };
256
257    #[fixture("tests/analyzer/graph/**/input.js")]
258    fn fixture(input: PathBuf) {
259        let input = RcStr::from(input.to_str().unwrap());
260        let rt = tokio::runtime::Builder::new_multi_thread()
261            .worker_threads(2)
262            .enable_all()
263            .build()
264            .unwrap();
265        rt.block_on(async move {
266            let tt = TurboTasks::new(TurboTasksBackend::new(
267                BackendOptions::default(),
268                noop_backing_storage(),
269            ));
270            tt.run_once(async move {
271                fixture_op(input).read_strongly_consistent().await?;
272                anyhow::Ok(())
273            })
274            .await
275            .unwrap();
276        });
277    }
278
279    #[turbo_tasks::function(operation, root)]
280    async fn fixture_op(input: RcStr) -> anyhow::Result<()> {
281        let input = PathBuf::from(input.as_str());
282        let graph_snapshot_path = input.with_file_name("graph.snapshot");
283        let graph_explained_snapshot_path = input.with_file_name("graph-explained.snapshot");
284        let graph_effects_snapshot_path = input.with_file_name("graph-effects.snapshot");
285        let resolved_explained_snapshot_path = input.with_file_name("resolved-explained.snapshot");
286        let resolved_effects_snapshot_path = input.with_file_name("resolved-effects.snapshot");
287        let large_marker = input.with_file_name("large");
288
289        let cm: Arc<SourceMap> = Arc::new(SourceMap::new(FilePathMapping::empty()));
290        let globals = Arc::new(Globals::new());
291
292        // Keep all non-`Send` SWC types (`SingleThreadedComments`, `Lrc<SourceFile>`)
293        // confined to this synchronous block so they don't have to cross an `.await`
294        // and break the `Send` bound on `tt.run_once`'s future.
295        let (eval_context, mut var_graph) = GLOBALS.set(&globals, || {
296            let fm = cm.load_file(&input).unwrap();
297            let comments = SingleThreadedComments::default();
298            let mut m = parse_file_as_program(
299                &fm,
300                Default::default(),
301                EsVersion::latest(),
302                Some(&comments),
303                &mut vec![],
304            )
305            .map_err(|err| anyhow::anyhow!("parse error: {err:?}"))?;
306
307            let unresolved_mark = Mark::new();
308            let top_level_mark = Mark::new();
309            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
310
311            let eval_context = EvalContext::new(
312                Some(&m),
313                unresolved_mark,
314                top_level_mark,
315                Default::default(),
316                Some(&comments),
317            );
318
319            let var_graph = create_graph(
320                &m,
321                &eval_context,
322                AnalyzeMode::CodeGenerationAndTracing,
323                true,
324            );
325            anyhow::Ok((eval_context, var_graph))
326        })?;
327        let var_cache = Default::default();
328
329        let mut named_values = var_graph
330            .values
331            .clone()
332            .into_iter()
333            .map(|((id, ctx), value)| {
334                let unique = var_graph.values.keys().filter(|(i, _)| &id == i).count() == 1;
335                if unique {
336                    (id.to_string(), ((id, ctx), value))
337                } else {
338                    (format!("{id}{ctx:?}"), ((id, ctx), value))
339                }
340            })
341            .collect::<Vec<_>>();
342        named_values.sort_by(|a, b| a.0.cmp(&b.0));
343
344        fn explain_all<'a>(
345            values: impl IntoIterator<Item = (&'a String, &'a JsValue, Option<AssignmentScopes>)>,
346        ) -> String {
347            values
348                .into_iter()
349                .map(|(id, value, assignment_scopes)| {
350                    let non_root_assignments = match assignment_scopes {
351                        Some(AssignmentScopes::AllInModuleEvalScope) => " (const after eval)",
352                        _ => "",
353                    };
354                    let (explainer, hints) = value.explain(10, 5);
355                    format!("{id}{non_root_assignments} = {explainer}{hints}")
356                })
357                .collect::<Vec<_>>()
358                .join("\n\n")
359        }
360
361        {
362            // Dump snapshot of graph
363
364            let large = large_marker.exists();
365
366            if !large {
367                NormalizedOutput::from(format!(
368                    "{:#?}",
369                    named_values
370                        .iter()
371                        .map(|(name, (_, value))| (name, value))
372                        .collect::<Vec<_>>()
373                ))
374                .compare_to_file(&graph_snapshot_path)
375                .unwrap();
376            }
377            NormalizedOutput::from(explain_all(named_values.iter().map(
378                |(name, (id, value))| {
379                    (
380                        name,
381                        value,
382                        eval_context.imports.assignment_scopes.get(id).copied(),
383                    )
384                },
385            )))
386            .compare_to_file(&graph_explained_snapshot_path)
387            .unwrap();
388            if !large {
389                NormalizedOutput::from(format!("{:#?}", var_graph.effects))
390                    .compare_to_file(&graph_effects_snapshot_path)
391                    .unwrap();
392            }
393        }
394
395        {
396            // Dump snapshot of resolved
397
398            let start = Instant::now();
399            let mut resolved = Vec::new();
400            for (name, (id, _)) in named_values.iter().cloned() {
401                let start = Instant::now();
402                // Ideally this would use eval_context.imports.get_attributes(span), but the
403                // span isn't available here
404                let (res, steps) = resolve(
405                    &var_graph,
406                    JsValue::Variable(id),
407                    ImportAttributes::empty_ref(),
408                    &var_cache,
409                )
410                .await;
411                let time = start.elapsed();
412                if time.as_millis() > 1 {
413                    println!(
414                        "linking {} {name} took {} in {} steps",
415                        input.display(),
416                        FormatDuration(time),
417                        steps
418                    );
419                }
420
421                resolved.push((name, res));
422            }
423            let time = start.elapsed();
424            if time.as_millis() > 1 {
425                println!("linking {} took {}", input.display(), FormatDuration(time));
426            }
427
428            let start = Instant::now();
429            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
430            let time = start.elapsed();
431            if time.as_millis() > 1 {
432                println!(
433                    "explaining {} took {}",
434                    input.display(),
435                    FormatDuration(time)
436                );
437            }
438
439            NormalizedOutput::from(explainer)
440                .compare_to_file(&resolved_explained_snapshot_path)
441                .unwrap();
442        }
443
444        {
445            // Dump snapshot of resolved effects
446
447            let start = Instant::now();
448            let mut resolved = Vec::new();
449            let mut queue = take(&mut var_graph.effects)
450                .into_iter()
451                .map(|effect| (0, effect))
452                .rev()
453                .collect::<Vec<_>>();
454            let mut i = 0;
455            while let Some((parent, effect)) = queue.pop() {
456                i += 1;
457                let start = Instant::now();
458                async fn handle_args(
459                    args: Vec<EffectArg>,
460                    queue: &mut Vec<(usize, Effect)>,
461                    var_graph: &VarGraph,
462                    var_cache: &Mutex<FxHashMap<Id, JsValue>>,
463                    i: usize,
464                ) -> Vec<JsValue> {
465                    let mut new_args = Vec::with_capacity(args.len());
466                    for arg in args {
467                        match arg {
468                            EffectArg::Value(v) => {
469                                new_args.push(
470                                    resolve(var_graph, v, ImportAttributes::empty_ref(), var_cache)
471                                        .await
472                                        .0,
473                                );
474                            }
475                            EffectArg::Closure(v, effects) => {
476                                new_args.push(
477                                    resolve(var_graph, v, ImportAttributes::empty_ref(), var_cache)
478                                        .await
479                                        .0,
480                                );
481                                queue.extend(effects.effects.into_iter().rev().map(|e| (i, e)));
482                            }
483                            EffectArg::Spread => {
484                                new_args.push(JsValue::unknown_empty(true, rcstr!("spread")));
485                            }
486                        }
487                    }
488                    new_args
489                }
490                let steps = match effect {
491                    Effect::Conditional {
492                        condition, kind, ..
493                    } => {
494                        let (condition, steps) = resolve(
495                            &var_graph,
496                            *condition,
497                            ImportAttributes::empty_ref(),
498                            &var_cache,
499                        )
500                        .await;
501                        resolved.push((format!("{parent} -> {i} conditional"), condition));
502                        match *kind {
503                            ConditionalKind::If { then } => {
504                                queue.extend(then.effects.into_iter().rev().map(|e| (i, e)));
505                            }
506                            ConditionalKind::Else { r#else } => {
507                                queue.extend(r#else.effects.into_iter().rev().map(|e| (i, e)));
508                            }
509                            ConditionalKind::IfElse { then, r#else }
510                            | ConditionalKind::Ternary { then, r#else } => {
511                                queue.extend(r#else.effects.into_iter().rev().map(|e| (i, e)));
512                                queue.extend(then.effects.into_iter().rev().map(|e| (i, e)));
513                            }
514                            ConditionalKind::IfElseMultiple { then, r#else } => {
515                                for then in then {
516                                    queue.extend(then.effects.into_iter().rev().map(|e| (i, e)));
517                                }
518                                for r#else in r#else {
519                                    queue.extend(r#else.effects.into_iter().rev().map(|e| (i, e)));
520                                }
521                            }
522                            ConditionalKind::And { expr }
523                            | ConditionalKind::Or { expr }
524                            | ConditionalKind::NullishCoalescing { expr }
525                            | ConditionalKind::Labeled { body: expr } => {
526                                queue.extend(expr.effects.into_iter().rev().map(|e| (i, e)));
527                            }
528                        };
529                        steps
530                    }
531                    Effect::Call {
532                        func,
533                        args,
534                        new,
535                        span,
536                        ..
537                    } => {
538                        let (func, steps) = resolve(
539                            &var_graph,
540                            *func,
541                            eval_context.imports.get_attributes(span),
542                            &var_cache,
543                        )
544                        .await;
545                        let new_args =
546                            handle_args(args, &mut queue, &var_graph, &var_cache, i).await;
547                        resolved.push((
548                            format!("{parent} -> {i} call"),
549                            if new {
550                                JsValue::new_from_iter(func, new_args)
551                            } else {
552                                JsValue::call_from_iter(func, new_args)
553                            },
554                        ));
555                        steps
556                    }
557                    Effect::FreeVar { var, .. } => {
558                        resolved.push((format!("{parent} -> {i} free var"), JsValue::FreeVar(var)));
559                        0
560                    }
561                    Effect::TypeOf { arg, .. } => {
562                        let (arg, steps) =
563                            resolve(&var_graph, *arg, ImportAttributes::empty_ref(), &var_cache)
564                                .await;
565                        resolved.push((
566                            format!("{parent} -> {i} typeof"),
567                            JsValue::type_of(Box::new(arg)),
568                        ));
569                        steps
570                    }
571                    Effect::MemberCall {
572                        obj, prop, args, ..
573                    } => {
574                        let (obj, obj_steps) =
575                            resolve(&var_graph, *obj, ImportAttributes::empty_ref(), &var_cache)
576                                .await;
577                        let (prop, prop_steps) =
578                            resolve(&var_graph, *prop, ImportAttributes::empty_ref(), &var_cache)
579                                .await;
580                        let new_args =
581                            handle_args(args, &mut queue, &var_graph, &var_cache, i).await;
582                        resolved.push((
583                            format!("{parent} -> {i} member call"),
584                            JsValue::member_call_from_iter(obj, prop, new_args),
585                        ));
586                        obj_steps + prop_steps
587                    }
588                    Effect::DynamicImport { args, .. } => {
589                        let new_args =
590                            handle_args(args, &mut queue, &var_graph, &var_cache, i).await;
591                        resolved.push((
592                            format!("{parent} -> {i} dynamic import"),
593                            JsValue::call_from_iter(JsValue::FreeVar("import".into()), new_args),
594                        ));
595                        0
596                    }
597                    Effect::Unreachable { .. } => {
598                        resolved.push((
599                            format!("{parent} -> {i} unreachable"),
600                            JsValue::unknown_empty(true, rcstr!("unreachable")),
601                        ));
602                        0
603                    }
604                    Effect::ImportMeta { .. }
605                    | Effect::ImportedBinding { .. }
606                    | Effect::Member { .. } => 0,
607                };
608                let time = start.elapsed();
609                if time.as_millis() > 1 {
610                    println!(
611                        "linking effect {} took {} in {} steps",
612                        input.display(),
613                        FormatDuration(time),
614                        steps
615                    );
616                }
617            }
618            let time = start.elapsed();
619            if time.as_millis() > 1 {
620                println!(
621                    "linking effects {} took {}",
622                    input.display(),
623                    FormatDuration(time)
624                );
625            }
626
627            let start = Instant::now();
628            let explainer = explain_all(resolved.iter().map(|(name, value)| (name, value, None)));
629            let time = start.elapsed();
630            if time.as_millis() > 1 {
631                println!(
632                    "explaining effects {} took {}",
633                    input.display(),
634                    FormatDuration(time)
635                );
636            }
637
638            NormalizedOutput::from(explainer)
639                .compare_to_file(&resolved_effects_snapshot_path)
640                .unwrap();
641        }
642
643        Ok(())
644    }
645
646    async fn resolve(
647        var_graph: &VarGraph,
648        val: JsValue,
649        attributes: &ImportAttributes,
650        var_cache: &Mutex<FxHashMap<Id, JsValue>>,
651    ) -> (JsValue, u32) {
652        // The caller (`fixture`) runs us inside `tt.run_once`, so a real
653        // turbo-tasks task context is already established here.
654        async {
655            let compile_time_info = CompileTimeInfo::builder(
656                Environment::new(ExecutionEnvironment::NodeJsLambda(
657                    NodeJsEnvironment {
658                        compile_target: CompileTarget {
659                            arch: Arch::X64,
660                            platform: Platform::Linux,
661                            endianness: Endianness::Little,
662                            libc: Libc::Glibc,
663                        }
664                        .resolved_cell(),
665                        node_version: NodeJsVersion::default().resolved_cell(),
666                        cwd: ResolvedVc::cell(None),
667                    }
668                    .resolved_cell(),
669                ))
670                .to_resolved()
671                .await?,
672            )
673            .cell()
674            .await?;
675            link(
676                var_graph,
677                val,
678                &super::test_utils::early_visitor,
679                &(|val| {
680                    Box::pin(super::test_utils::visitor(
681                        val,
682                        compile_time_info,
683                        attributes,
684                    ))
685                }),
686                &Default::default(),
687                var_cache,
688            )
689            .await
690        }
691        .await
692        .unwrap()
693    }
694}