diff --git a/src/aot/lower/transpiled.rs b/src/aot/lower/transpiled.rs index bc3262772b2a..3be0c209118d 100644 --- a/src/aot/lower/transpiled.rs +++ b/src/aot/lower/transpiled.rs @@ -146,6 +146,7 @@ impl RuntimeTranspiler<'_> { options.tree_shaking = true; options.warn_about_unbundled_modules = false; options.use_define_for_class_fields = !request.assigns_class_fields; + options.keep_parameter_lists_as_written = true; let features = &mut options.features; features.standard_decorators = !request.has_legacy_decorators; features.allow_runtime = true; diff --git a/src/ast/ast_result.rs b/src/ast/ast_result.rs index 1ab68574a4fd..10701d94071b 100644 --- a/src/ast/ast_result.rs +++ b/src/ast/ast_result.rs @@ -10,8 +10,8 @@ use bun_collections::{ArrayHashMap, StringArrayHashMap, StringHashMap}; use crate::runtime; use crate::{ - CharFreq, ExportsKind, Expr, InlinedEnumValue, LocRef, NamedExport, NamedImport, Part, Range, - Ref, Scope, SlotCounts, StoreStr, Target, + CharFreq, ExportsKind, Expr, InlinedEnumValue, LocRef, NamedExport, NamedImport, + ParameterListsAsWritten, Part, Range, Ref, Scope, SlotCounts, StoreStr, Target, }; use crate::part::List as PartList; @@ -104,6 +104,8 @@ pub struct Ast<'a> { pub import_meta_ref: Ref, /// Output depends on more than the source text (macros, `import.meta.glob`), so don't cache it. pub depends_on_more_than_source: bool, + /// Filled for a module the runtime loads. + pub parameter_lists_as_written: ParameterListsAsWritten, } // `parts`/`symbols`/`import_records` are now `ArenaVec`s and need an allocator, @@ -150,8 +152,14 @@ impl<'a> Ast<'a> { has_import_meta: false, import_meta_ref: Ref::NONE, depends_on_more_than_source: false, + parameter_lists_as_written: Default::default(), } } + + /// What the printer takes: `None` when no list is kept, so that it tests one pointer for each function. + pub fn kept_parameter_lists(&self) -> Option<&ParameterListsAsWritten> { + (!self.parameter_lists_as_written.is_empty()).then_some(&self.parameter_lists_as_written) + } } pub struct CommonJSNamedExport { diff --git a/src/ast/lib.rs b/src/ast/lib.rs index 586657d54ac8..3b826b688deb 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -2623,6 +2623,7 @@ pub mod runtime; pub mod ast_result; pub mod import_record; pub mod loader; +pub mod parameter_list_as_written; pub mod target; pub use ast_result::{ @@ -2631,6 +2632,7 @@ pub use ast_result::{ }; pub use import_record::{Flags as ImportRecordFlags, ImportRecord, Tag as ImportRecordTag}; pub use loader::{Loader, LoaderHashTable, SideEffects}; +pub use parameter_list_as_written::ParameterListsAsWritten; pub use target::Target; pub mod transpiler_cache; // Glob re-export: `link_interface!` emits `#[doc(hidden)]` type aliases that diff --git a/src/ast/parameter_list_as_written.rs b/src/ast/parameter_list_as_written.rs new file mode 100644 index 000000000000..d88605a8afc7 --- /dev/null +++ b/src/ast/parameter_list_as_written.rs @@ -0,0 +1,282 @@ +//! The runtime prints a parameter list that holds a comment as it is written: an injector reads `/* token */ name` from `fn.toString()`. + +use bun_alloc::AstAlloc; +use bun_collections::ArrayHashMap; +use bun_collections::array_hash_map::AutoContext; +use bun_core::strings; + +use crate::StoreStr; + +/// The key is where the printer finds the list again: the `(` of a function, the start of an arrow function. +pub type ParameterListsAsWritten = ArrayHashMap; + +/// `mentionsOriginalModule` (src/jsc/bindings/BunPlugin.cpp) takes each of these, anywhere in the text of a `mock.module` factory, as a mention of the original module. +const READ_BY_MOCK_MODULE: [&[u8]; 3] = [b"importOriginal", b"importActual", b"requireActual"]; + +/// The text up to the `)` of the list that starts at `after_open_paren`, if the printer is to write it: plain names and a comment. +pub fn text_to_keep(after_open_paren: &[u8]) -> Option<&[u8]> { + let mut names = Names::new(after_open_paren); + while names.next_name().ok()?.is_some() {} + let (text, rest) = after_open_paren.split_at(names.position); + let is_kept = names.has_comment + && rest.first() == Some(&b')') + && !READ_BY_MOCK_MODULE + .iter() + .any(|word| strings::contains(text, word)); + is_kept.then_some(text) +} + +pub struct Name { + pub start: usize, + pub end: usize, + /// `...` stands before it. + pub is_rest: bool, +} + +/// The text is not a list of names with a comma between them, `...` before the last at most, ASCII whitespace and ASCII comments. +#[derive(Debug)] +pub struct NotPlainNames; + +/// The names in the text between the parentheses of a parameter list. +pub struct Names<'a> { + text: &'a [u8], + position: usize, + has_comment: bool, + /// A name was passed, and no comma after it. + is_after_name: bool, + /// That name has `...` before it: nothing but whitespace and comments follows it. + is_after_rest: bool, +} + +impl<'a> Names<'a> { + pub fn new(text: &'a [u8]) -> Self { + Names { + text, + position: 0, + has_comment: false, + is_after_name: false, + is_after_rest: false, + } + } + + /// `Ok(None)` at the end of the list: the end of the text, or a `)`. + pub fn next_name(&mut self) -> Result, NotPlainNames> { + let text = self.text; + let mut at = self.position; + let mut is_rest = false; + let found = loop { + match text.get(at).copied() { + Some(b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C) => at += 1, + Some(b'/') => match end_of_comment(text, at) { + Some(end) => { + at = end; + self.has_comment = true; + } + None => break Err(NotPlainNames), + }, + None | Some(b')') if !is_rest => break Ok(None), + Some(b',') if self.is_after_name && !self.is_after_rest => { + self.is_after_name = false; + at += 1; + } + Some(b'.') if !self.is_after_name && !is_rest && text[at..].starts_with(b"...") => { + is_rest = true; + at += 3; + } + Some(b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$') if !self.is_after_name => { + let start = at; + while text.get(at).is_some_and(|&byte| is_name_byte(byte)) { + at += 1; + } + self.is_after_name = true; + self.is_after_rest = is_rest; + break Ok(Some(Name { + start, + end: at, + is_rest, + })); + } + _ => break Err(NotPlainNames), + } + }; + self.position = at; + found + } +} + +fn is_name_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$' +} + +/// `text[at]` is `/`. The end of the ASCII comment that starts there, if it ends inside the text: a line comment ends before its line terminator. +fn end_of_comment(text: &[u8], at: usize) -> Option { + let body = text.get(at + 2..)?; + let length = match (text[at + 1], body.first()) { + // The printer writes a comment that starts with `!` by itself, into the body of the function. + (_, Some(b'!')) => return None, + (b'*', _) => strings::index_of(body, b"*/")? + 2, + // JavaScriptCore reads `sourceURL=` and `sourceMappingURL=` from a comment that starts with `//#` or `//@`. + (b'/', Some(b'#' | b'@')) => return None, + (b'/', _) => strings::index_of_any(body, b"\r\n")?, + _ => return None, + }; + body[..length].is_ascii().then_some(at + 2 + length) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `None`: not a list of plain names. + fn names_of(text: &str) -> Option<(Vec, bool)> { + let mut names = Names::new(text.as_bytes()); + let mut found = Vec::new(); + while let Some(name) = names.next_name().ok()? { + let spelling = &text[name.start..name.end]; + found.push(if name.is_rest { + format!("...{spelling}") + } else { + spelling.to_owned() + }); + } + Some((found, names.has_comment)) + } + + fn commented(names: &[&str]) -> Option<(Vec, bool)> { + Some((names.iter().map(|&name| name.to_owned()).collect(), true)) + } + + fn kept(after_open_paren: &str) -> Option<&str> { + text_to_keep(after_open_paren.as_bytes()).map(|text| core::str::from_utf8(text).unwrap()) + } + + #[test] + fn names_and_comments() { + assert_eq!( + names_of("/* config */config, /* config.proxies */proxies"), + commented(&["config", "proxies"]) + ); + assert_eq!( + names_of("a /* after */, $b_1 // line\n"), + commented(&["a", "$b_1"]) + ); + assert_eq!(names_of("/* a, b) */"), commented(&[])); + assert_eq!( + names_of(" a,\r\n\tb, "), + Some((vec!["a".into(), "b".into()], false)) + ); + assert_eq!(names_of(""), Some((vec![], false))); + } + + #[test] + fn rest_parameter() { + assert_eq!( + names_of("a, /* all */ ...rest"), + commented(&["a", "...rest"]) + ); + assert_eq!(names_of("... /* all */ rest"), commented(&["...rest"])); + assert_eq!(names_of("a, ..."), None); + assert_eq!(names_of("......a"), None); + assert_eq!(names_of("..a"), None); + assert_eq!(names_of("...,a"), None); + } + + #[test] + fn what_is_not_a_plain_name() { + for text in [ + "a = 1", + "{ a }", + "[a]", + "a: number", + "a?", + "@inject a", + "a /* \u{e9} */", + "a // \u{e9}\n", + "\u{e9}", + "a\\u0062", + "a