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 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 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 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 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 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 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 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 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}