Skip to main content

turbopack_ecmascript/references/
cross_module_constants.rs

1use std::hash::Hash;
2
3use anyhow::{Context, Result, bail};
4use async_trait::async_trait;
5use bincode::{Decode, Encode};
6use num_bigint::BigInt;
7use parking_lot::Mutex;
8use rustc_hash::FxHashMap;
9use swc_core::common::{GLOBALS, source_map::SmallPos};
10use thread_local::ThreadLocal;
11use tracing::instrument;
12use turbo_rcstr::{RcStr, rcstr};
13use turbo_tasks::{NonLocalValue, ResolvedVc, TryJoinIterExt, Vc, trace::TraceRawVcs};
14use turbo_tasks_fs::FileSystemPath;
15use turbopack_core::{
16    compile_time_info::CompileTimeInfo,
17    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
18    module::Module,
19    reference::ModuleReference,
20};
21
22use crate::{
23    AnalyzeMode, EcmascriptParsable, SpecifiedModuleType,
24    analyzer::{
25        Bump, BumpVec, ConstantValue, JsValue, Modified, ModuleValue, ObjectMutability, ObjectPart,
26        builtin::replace_builtin, graph::create_graph, linker::link,
27        well_known::replace_well_known,
28    },
29    directive::parse_module_turbopack_directives,
30    parse::ParseResult,
31    references::{early_value_visitor, esm::EsmAssetReference},
32};
33
34const STRING_INLINE_THRESHOLD: usize = 6;
35const NUMBER_INLINE_THRESHOLD: f64 = 1_000_000.0;
36const BIGINT_INLINE_THRESHOLD: i64 = 1_000_000;
37
38/// Import names that are all-uppercase and contain at least one letter are eligible for automatic
39/// constant inlining, even without an import attribute.
40pub fn is_import_name_eligible_for_exports(name: &str) -> bool {
41    let mut seen_alphabetic = false;
42    for c in name.chars() {
43        if !(c.is_ascii() && (!c.is_ascii_alphabetic() || c.is_uppercase())) {
44            return false;
45        }
46        seen_alphabetic |= c.is_ascii_alphabetic();
47    }
48    seen_alphabetic
49}
50
51#[instrument(level = "info", skip_all, name = "determine cross-module constants")]
52pub async fn module_value_to_constants_module<'a>(
53    arena: &'a ThreadLocal<Bump>,
54    module_value: &ModuleValue,
55    compile_time_info: Vc<CompileTimeInfo>,
56    import_references: &[ResolvedVc<EsmAssetReference>],
57) -> Result<Option<JsValue<'a>>> {
58    let Some(reference_idx) = module_value.reference else {
59        bail!("missing reference for constant value");
60    };
61
62    let reference_idx = reference_idx.get();
63    let import_reference = import_references
64        .get(reference_idx)
65        .with_context(|| format!("couldn't find import reference at index {reference_idx}"))?;
66
67    // We are reusing the exact resolve options from EsmAssetReference here, which is good and gives
68    // us side-effect-free barrel file resolving for free.
69    let resolved = import_reference.resolve_reference().await?;
70    let Some(module) = resolved.first_module().await? else {
71        // failed to resolve, issue was already emitted by resolve_reference
72        return Ok(None);
73    };
74
75    let constants = get_constants(*module, compile_time_info).await?;
76
77    Ok(constants.as_ref().map(|constants| {
78        constants.as_js_value(
79            arena.get_or_default(),
80            module_value
81                .annotations
82                .as_ref()
83                .and_then(|a| a.turbopack_constants()),
84        )
85    }))
86}
87
88#[derive(Debug, Clone, Eq, PartialEq, NonLocalValue, TraceRawVcs, Encode, Decode)]
89enum ConstantsModuleExport {
90    Constant(ConstantValueBitEquality),
91    NonConstant(ResolvedVc<NonConstantIssue>),
92}
93
94#[turbo_tasks::value]
95#[derive(Debug)]
96struct ConstantsModule {
97    exports: Vec<(RcStr, ConstantsModuleExport)>,
98    has_directive: bool,
99}
100
101#[turbo_tasks::value(transparent)]
102#[derive(Debug)]
103struct OptionConstantsModule(Option<ConstantsModule>);
104
105impl ConstantsModule {
106    pub fn as_js_value<'a>(
107        &self,
108        arena: &'a Bump,
109        constant_annotation: Option<bool>,
110    ) -> JsValue<'a> {
111        let has_opt_in = constant_annotation.unwrap_or(self.has_directive);
112
113        // This has to be
114        // - mutable:false, otherwise nothing would ever be inlined, because all property accesses
115        //   would be receive a `|unknown` alternative
116        // - frozen:true, otherwise mutable:false would cause accesses of missing properties to be
117        //   `undefined`. Because we return a JsValue::Object even if the module has only some
118        //   constants exports, this would cause `import {NON_CONSTANT_EXPORT}` to be incorrectly
119        //   replaced with `undefined`.
120        JsValue::object_with_mutability(
121            BumpVec::from_iter_in(
122                arena,
123                self.exports.iter().map(|(key, value)| {
124                    ObjectPart::KeyValue(
125                        JsValue::Constant(ConstantValue::Str(key.clone().into())),
126                        match value {
127                            ConstantsModuleExport::Constant(value) => {
128                                if !has_opt_in {
129                                    // when not having opt in, only inline short literals
130                                    match &value.0 {
131                                        ConstantValue::Str(s)
132                                            if s.as_str().len() > STRING_INLINE_THRESHOLD =>
133                                        {
134                                            JsValue::unknown_empty(
135                                                false,
136                                                rcstr!("constant too long"),
137                                            )
138                                        }
139                                        ConstantValue::Num(n)
140                                            if n.0.abs() > NUMBER_INLINE_THRESHOLD =>
141                                        {
142                                            JsValue::unknown_empty(
143                                                false,
144                                                rcstr!("constant too long"),
145                                            )
146                                        }
147                                        ConstantValue::BigInt(n)
148                                            if **n > BigInt::from(BIGINT_INLINE_THRESHOLD)
149                                                || **n < BigInt::from(-BIGINT_INLINE_THRESHOLD) =>
150                                        {
151                                            JsValue::unknown_empty(
152                                                false,
153                                                rcstr!("constant too long"),
154                                            )
155                                        }
156                                        ConstantValue::Regex(_) => {
157                                            // Regexes are literals, but they are also objects, so
158                                            // have identity and aren't inlined without opt in.
159                                            JsValue::unknown_empty(
160                                                false,
161                                                rcstr!("regex not inlined"),
162                                            )
163                                        }
164                                        v => JsValue::Constant(v.clone()),
165                                    }
166                                } else {
167                                    JsValue::Constant(value.0.clone())
168                                }
169                            }
170                            ConstantsModuleExport::NonConstant(issue) => {
171                                if constant_annotation == Some(true) {
172                                    // If self.has_directive, then we already emitted the issue in
173                                    // get_constants.
174                                    issue.emit();
175                                }
176                                JsValue::unknown_empty(false, rcstr!("not a constant"))
177                            }
178                        },
179                    )
180                }),
181            ),
182            // TODO ideally this would just use ObjectMutability::Frozen.
183            //
184            // When not opted in, this has to stay FrozenSubset though, because when importing a
185            // non-constant export, it should not be replaced with `undefined` (which is what
186            // Frozen) would do.
187            ObjectMutability::FrozenSubset,
188        )
189    }
190}
191
192#[turbo_tasks::function]
193pub async fn get_constants(
194    module: ResolvedVc<Box<dyn Module>>,
195    compile_time_info: Vc<CompileTimeInfo>,
196) -> Result<Vc<OptionConstantsModule>> {
197    let Some(parseable) = ResolvedVc::try_sidecast::<Box<dyn EcmascriptParsable>>(module) else {
198        // should never actually happen, there should be a "imported module is not chunkable" error
199        // somewhere as well if it's truly not an Ecmascript module
200        return Ok(Vc::cell(None));
201    };
202
203    let parsed = parseable.failsafe_parse().await?;
204    let ParseResult::Ok {
205        program,
206        eval_context,
207        globals,
208        ..
209    } = &*parsed
210    else {
211        // The `parse` call has already emitted parse issues in case of `ParseResult::Unparsable`
212        return Ok(Vc::cell(None));
213    };
214
215    let directives = parse_module_turbopack_directives(program);
216
217    let arena = ThreadLocal::new();
218
219    let var_graph = {
220        let supports_block_scoping = *compile_time_info
221            .environment()
222            .runtime_versions()
223            .supports_block_scoping()
224            .await?;
225        let _span = tracing::trace_span!("analyze variable values").entered();
226        GLOBALS.set(globals, || {
227            create_graph(
228                arena.get_or_default(),
229                program,
230                eval_context,
231                AnalyzeMode::Tracing,
232                supports_block_scoping,
233                // This is currently ignored with cjs_tree_shaking:false
234                SpecifiedModuleType::Automatic,
235                // TODO enable CJS tree shaking here
236                false,
237                // TODO enable CJS scope hoisting here
238                false,
239            )
240        })
241    };
242
243    let fun_args_values = Mutex::new(FxHashMap::default());
244    let var_cache = Mutex::new(FxHashMap::default());
245
246    let compile_time_info_ref = compile_time_info.await?;
247
248    let exports = eval_context
249        .imports
250        .exports_ids
251        .iter()
252        .map(async |(export_name, (binding, span))| {
253            let value = GLOBALS.set(globals, || {
254                eval_context.eval_id(arena.get_or_default(), binding.clone())
255            });
256
257            let linked_value = link(
258                &arena,
259                &var_graph,
260                value.clone_in(arena.get_or_default()),
261                &|value| early_value_visitor(&arena, value),
262                &async |v| {
263                    if let [Some((name, _))] = &*v.get_definable_name(Some(&var_graph))
264                        && let Some(value) = compile_time_info_ref.defines.get(name).await?
265                    {
266                        return Ok((
267                            JsValue::from_compile_time_define_value_in(
268                                arena.get_or_default(),
269                                &value,
270                            )?,
271                            Modified::Yes,
272                        ));
273                    }
274
275                    // This is basically what's necessary to support imports in constant modules.
276                    // It's just that you'd need `get_constants_inner` which is not a turbotask (and
277                    // contains the logic of the current get_constants function). So that would redo
278                    // a small amount of work but would allow imports.
279                    //
280                    // TODO when opted in, also resolve imports
281                    // if directives.constants_module
282                    //     && let JsValue::Module(module) = &v
283                    // {
284                    //     // We can't do a recursive turbotask call here, to prevent deadlocks.
285                    //     if let Some(constants) =
286                    //         get_constants(resolve_somehow(module), compile_time_info)
287                    //             .await?
288                    //             .as_ref()
289                    //     {
290                    //         return Ok((constants.as_js_value(false), true));
291                    //     }
292                    // }
293
294                    let (mut v, mut modified) =
295                        replace_well_known(&arena, v, compile_time_info, false).await?;
296                    if replace_builtin(arena.get_or_default(), &mut v).is_modified() {
297                        modified = Modified::Yes;
298                    }
299                    if !modified.is_modified() {
300                        modified = Modified::from(v.make_nested_operations_unknown());
301                    }
302                    Ok((v, modified))
303                },
304                &fun_args_values,
305                &var_cache,
306            )
307            .await?;
308
309            if let JsValue::Constant(constant) = linked_value.0 {
310                Ok((
311                    export_name.as_str().into(),
312                    ConstantsModuleExport::Constant(ConstantValueBitEquality(constant)),
313                ))
314            } else {
315                let explained = linked_value.0.explain(10, 5);
316                let issue = NonConstantIssue::new(
317                    export_name.as_str().into(),
318                    module.ident().await?.path.clone(),
319                    module.source().await?.map(|source| {
320                        IssueSource::from_swc_offsets(source, span.lo.to_u32(), span.hi.to_u32())
321                    }),
322                    (explained.0.into(), explained.1.into()),
323                )
324                .to_resolved()
325                .await?;
326                if directives.constants_module {
327                    issue.emit();
328                }
329                Ok((
330                    export_name.as_str().into(),
331                    ConstantsModuleExport::NonConstant(issue),
332                ))
333            }
334        })
335        .try_join()
336        .await?;
337
338    Ok(Vc::cell(Some(ConstantsModule {
339        exports,
340        has_directive: directives.constants_module,
341    })))
342}
343
344#[turbo_tasks::value]
345struct NonConstantIssue {
346    export: RcStr,
347    file_path: FileSystemPath,
348    source: Option<IssueSource>,
349    value: (RcStr, RcStr),
350}
351
352#[turbo_tasks::value_impl]
353impl NonConstantIssue {
354    #[turbo_tasks::function]
355    fn new(
356        export: RcStr,
357        file_path: FileSystemPath,
358        source: Option<IssueSource>,
359        value: (RcStr, RcStr),
360    ) -> Vc<Self> {
361        Self {
362            export,
363            file_path,
364            source,
365            value,
366        }
367        .cell()
368    }
369}
370
371#[async_trait]
372#[turbo_tasks::value_impl]
373impl Issue for NonConstantIssue {
374    fn severity(&self) -> IssueSeverity {
375        IssueSeverity::Error
376    }
377
378    async fn title(&self) -> Result<StyledString> {
379        Ok(StyledString::Line(vec![
380            StyledString::Text(rcstr!("Export ")),
381            StyledString::Code(self.export.clone()),
382            StyledString::Text(rcstr!(" is not a constant")),
383        ]))
384    }
385
386    fn stage(&self) -> IssueStage {
387        IssueStage::Analysis
388    }
389
390    async fn file_path(&self) -> Result<FileSystemPath> {
391        Ok(self.file_path.clone())
392    }
393
394    async fn description(&self) -> Result<Option<StyledString>> {
395        Ok(Some(StyledString::Stack(
396            [
397                Some(StyledString::Line(vec![
398                    StyledString::Text(rcstr!("It was analyzed to be ")),
399                    StyledString::Code(self.value.0.clone()),
400                ])),
401                (!self.value.1.is_empty())
402                    .then(|| StyledString::Line(vec![StyledString::Code(self.value.1.clone())])),
403                Some(StyledString::Line(vec![
404                    StyledString::Text(rcstr!(
405                        "It has to be a constant because the module contains "
406                    )),
407                    StyledString::Code(rcstr!("use turbopack: constants")),
408                    StyledString::Text(rcstr!(" or was imported with ")),
409                    StyledString::Code(rcstr!("with {turbopackConstants: 'true'}")),
410                ])),
411            ]
412            .into_iter()
413            .flatten()
414            .collect(),
415        )))
416    }
417
418    fn source(&self) -> Option<IssueSource> {
419        self.source
420    }
421}
422
423#[derive(Debug, Clone, Default, TraceRawVcs, Encode, Decode, NonLocalValue)]
424struct ConstantValueBitEquality(ConstantValue);
425
426impl PartialEq for ConstantValueBitEquality {
427    fn eq(&self, other: &Self) -> bool {
428        match (&self.0, &other.0) {
429            (ConstantValue::Undefined, ConstantValue::Undefined)
430            | (ConstantValue::Null, ConstantValue::Null)
431            | (ConstantValue::True, ConstantValue::True)
432            | (ConstantValue::False, ConstantValue::False) => true,
433            (ConstantValue::Num(l), ConstantValue::Num(r)) => {
434                l.0.to_le_bytes() == r.0.to_le_bytes()
435            }
436            (ConstantValue::BigInt(l), ConstantValue::BigInt(r)) => l == r,
437            (ConstantValue::Str(l), ConstantValue::Str(r)) => l == r,
438            (ConstantValue::Regex(l), ConstantValue::Regex(r)) => l == r,
439            _ => false,
440        }
441    }
442}
443impl Eq for ConstantValueBitEquality {}
444
445impl Hash for ConstantValueBitEquality {
446    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
447        std::mem::discriminant(&self.0).hash(state);
448        match &self.0 {
449            ConstantValue::Undefined => {}
450            ConstantValue::Null => {}
451            ConstantValue::True => {}
452            ConstantValue::False => {}
453            ConstantValue::Num(n) => n.0.to_le_bytes().hash(state),
454            ConstantValue::BigInt(n) => n.hash(state),
455            ConstantValue::Str(s) => s.hash(state),
456            ConstantValue::Regex(r) => r.hash(state),
457        }
458    }
459}