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
38pub 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 let resolved = import_reference.resolve_reference().await?;
70 let Some(module) = resolved.first_module().await? else {
71 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 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 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 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 issue.emit();
175 }
176 JsValue::unknown_empty(false, rcstr!("not a constant"))
177 }
178 },
179 )
180 }),
181 ),
182 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 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 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 SpecifiedModuleType::Automatic,
235 false,
237 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 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}