Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 127 additions & 1 deletion src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ impl<'a> ImportRecordList<'a> {
}
}

/// `import X = A.B` in TypeScript, kept from the parse pass for [`P::bind_ts_import_equals_aliases`].
pub(crate) struct TsImportEqualsAlias<'a> {
pub(crate) alias: Ref,
pub(crate) scope: js_ast::StoreRef<Scope>,
pub(crate) target: Expr,
pub(crate) is_export: bool,
pub(crate) next: Option<&'a TsImportEqualsAlias<'a>>,
}

/// See [`P::parser_snapshot`].
pub(crate) struct ParserSnapshot<'a> {
lexer: js_lexer::LexerSnapshot<'a>,
Expand Down Expand Up @@ -700,6 +709,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
pub(crate) ts_namespace_scopes: Vec<js_ast::StoreRef<js_ast::TSNamespaceScope>>,
/// Companion list (sibling scopes share a map, so the scope list can't free it).
pub(crate) ts_namespace_member_maps: Vec<js_ast::StoreRef<js_ast::TSNamespaceMemberMap>>,
/// Every `import X = A.B` of the file, newest first.
pub(crate) ts_import_equals_aliases: Option<&'a TsImportEqualsAlias<'a>>,
/// When visiting expressions, namespace metadata is associated with the most
/// recently visited node. If namespace metadata is present, "tsNamespaceTarget"
/// will be set to the most recently visited node (as a way to mark that this
Expand Down Expand Up @@ -3399,6 +3410,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

self.hoist_symbols(self.module_scope_ref());

if TYPESCRIPT && self.ts_import_equals_aliases.is_some() {
self.bind_ts_import_equals_aliases();
}

let mut generated_symbols_count: u32 = 3;

if self.options.features.react_fast_refresh {
Expand Down Expand Up @@ -4779,6 +4794,104 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(())
}

/// Gives each `import X = A.B` that names an enum or a namespace of this file the members of what it names.
#[cold]
#[inline(never)]
fn bind_ts_import_equals_aliases(&mut self) {
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Waiting,
InProgress,
Done,
}
let mut records: Vec<&'a TsImportEqualsAlias<'a>> = Vec::new();
let mut next = self.ts_import_equals_aliases;
while let Some(record) = next {
records.push(record);
next = record.next;
}
records.reverse();
let mut states = vec![State::Waiting; records.len()];
// Only an alias that names an alias written behind it needs this.
let mut index_of_alias: Option<HashMap<Ref, u32>> = None;
let mut in_progress: Vec<u32> = Vec::new();
let mut names: Vec<&[u8]> = Vec::new();
for first in 0..records.len() as u32 {
if states[first as usize] != State::Waiting {
continue;
}
in_progress.push(first);
'record: while let Some(&index) = in_progress.last() {
let record = records[index as usize];
states[index as usize] = State::InProgress;
names.clear();
let mut root = record.target;
while let js_ast::ExprData::EDot(dot) = root.data {
names.push(dot.name.slice());
root = dot.target;
}
// The bare name of an `export import` of a namespace has no binding to print.
let is_skipped = record.is_export
&& (record.scope.ts_namespace.is_some()
|| self.is_export_to_eliminate(record.alias));
let mut members = None;
if let js_ast::ExprData::EIdentifier(first_name) = root.data
&& !is_skipped
{
let first_name = self.load_name_from_ref(first_name.ref_);
use crate::scan::scan_symbols::{TsNameBinding, find_ts_name_binding};
members = match find_ts_name_binding(record.scope, first_name) {
TsNameBinding::Members(members) => Some(members),
TsNameBinding::Nothing => None,
TsNameBinding::Symbol(bound) => {
match self.ref_to_ts_namespace_member.get(&bound) {
Some(&js_ast::ts::Data::Namespace(members)) => Some(members),
Some(_) => None,
None => {
let is_constant = self.symbols[bound.inner_index() as usize]
.kind
== js_ast::symbol::Kind::Constant;
if is_constant {
let index_of_alias =
index_of_alias.get_or_insert_with(|| {
let mut indexes = HashMap::default();
for (index, record) in records.iter().enumerate() {
indexes.insert(record.alias, index as u32);
}
indexes
});
if let Some(&other) = index_of_alias.get(&bound)
&& states[other as usize] == State::Waiting
{
in_progress.push(other);
continue 'record;
}
}
None
}
}
}
};
}
for name in names.iter().rev() {
members = members.and_then(|members| {
let members: &js_ast::TSNamespaceMemberMap = &members;
match members.get(name).map(|member| &member.data) {
Some(&js_ast::ts::Data::Namespace(members)) => Some(members),
_ => None,
}
});
}
if let Some(members) = members {
self.ref_to_ts_namespace_member
.insert(record.alias, js_ast::ts::Data::Namespace(members));
}
states[index as usize] = State::Done;
in_progress.pop();
}
}
}

// Generate a TypeScript namespace object for this namespace's scope. If this
// namespace is another block that is to be merged with an existing namespace,
// use that earlier namespace's object instead.
Expand Down Expand Up @@ -6683,7 +6796,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
loop {
match &current.data {
js_ast::ExprData::EIdentifier(id) => {
self.ignore_usage(id.ref_);
// An `import X = A.B` alias keeps its use, so that its statement stays.
let is_alias = TYPESCRIPT
&& self.symbols[id.ref_.inner_index() as usize].kind
== js_ast::symbol::Kind::Constant;
if !is_alias {
self.ignore_usage(id.ref_);
}
}
js_ast::ExprData::EDot(dot) => {
current = dot.target;
Expand Down Expand Up @@ -8334,6 +8453,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.scopes_in_order_for_enum.pop();
}

debug_assert!(
self.ts_import_equals_aliases.is_none_or(|newest| {
(newest.alias.inner_index() as usize) < snapshot.symbols_len
})
);

// Enum and namespace symbols also key `ref_to_ts_namespace_member`; a symbol
// that later reuses the index must not inherit their namespace data.
if self.symbols.len() > snapshot.symbols_len {
Expand Down Expand Up @@ -9882,6 +10007,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
ref_to_ts_namespace_member: Default::default(),
ts_namespace_scopes: Vec::new(),
ts_namespace_member_maps: Vec::new(),
ts_import_equals_aliases: None,
ts_namespace: RecentlyVisitedTSNamespace {
expr: null_expr_data(),
map: None,
Expand Down
14 changes: 13 additions & 1 deletion src/js_parser/parse/parse_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
loc: name_loc,
ref_: Ref::NONE,
};
let aliases_before = p.ts_import_equals_aliases;
let scope_index = p.push_scope_for_parse_pass(ScopeKind::Entry, loc)?;
p.current_scope_mut().ts_namespace = Some(ts_namespace);

Expand Down Expand Up @@ -399,6 +400,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
|| opts.is_typescript_declare
{
p.pop_and_discard_scope(scope_index);
p.ts_import_equals_aliases = aliases_before;
if opts.scope.is_module() {
p.local_type_names.put(name_text, true)?;
}
Expand Down Expand Up @@ -503,7 +505,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let mut value = target;
p.lexer.expect(T::TIdentifier)?;

if name == b"require" && p.lexer.token == T::TOpenParen {
let is_require = name == b"require" && p.lexer.token == T::TOpenParen;
if is_require {
// "import ns = require('x')"
p.lexer.next()?;
let path_estr = p.lexer.to_e_string()?;
Expand Down Expand Up @@ -557,6 +560,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let ref_ = p
.declare_symbol(SymbolKind::Constant, default_name_loc, default_name)
.expect("unreachable");
if !is_require {
p.ts_import_equals_aliases = Some(p.arena.alloc(crate::p::TsImportEqualsAlias {
alias: ref_,
scope: p.current_scope,
target: value,
is_export: opts.is_export,
next: p.ts_import_equals_aliases,
}));
}
let binding = p.b(B::Identifier { r#ref: ref_ }, default_name_loc);
let decls = G::DeclList::init_one(G::Decl {
binding,
Expand Down
31 changes: 31 additions & 0 deletions src/js_parser/scan/scan_symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ use crate::parser::FindSymbolResult;
use bun_ast as js_ast;
use bun_ast::{Ref, Scope};

/// What a name is bound to, for [`P::bind_ts_import_equals_aliases`].
pub(crate) enum TsNameBinding {
Symbol(Ref),
Members(js_ast::StoreRef<js_ast::TSNamespaceMemberMap>),
Nothing,
}

/// The scope walk of `find_symbol_with_record_usage`, without a use and without a new symbol.
pub(crate) fn find_ts_name_binding(scope: js_ast::StoreRef<Scope>, name: &[u8]) -> TsNameBinding {
let hash = Scope::get_member_hash(name);
let mut current = Some(scope);
while let Some(scope) = current {
if let Some(member) = scope.get_member_with_hash(name, hash) {
return TsNameBinding::Symbol(member.ref_);
}
if let Some(ts_namespace) = scope.ts_namespace {
let exported: &js_ast::TSNamespaceMemberMap = &ts_namespace.exported_members;
if let Some(member) = exported.get(name) {
if member.data.is_enum() == ts_namespace.is_enum_scope {
return match member.data {
js_ast::ts::Data::Namespace(members) => TsNameBinding::Members(members),
_ => TsNameBinding::Nothing,
};
}
}
}
current = scope.parent;
}
TsNameBinding::Nothing
}

impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> {
pub(crate) fn find_symbol(
&mut self,
Expand Down
29 changes: 28 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1779,7 +1779,8 @@ pub(crate) mod __gated_printer {
}
// "**" can't contain certain unary expressions
Op::Code::BinPow => {
match &e.left.data {
// An inlined enum member prints as its value, which can be a negative number.
match e.left.unwrap_inlined().data {
ExprData::EUnary(left) => {
if Op::Code::unary_assign_target(left.op) == js_ast::AssignTarget::None
{
Expand All @@ -1795,6 +1796,10 @@ pub(crate) mod __gated_printer {
v.left_level = Level::Call;
}
}
// A member of an enum of another module prints as its value too.
_ if self.prints_as_imported_enum_value(&e.left) => {
v.left_level = Level::Call;
}
_ => {}
}
}
Expand Down Expand Up @@ -6749,6 +6754,28 @@ pub(crate) mod __gated_printer {
))
}

fn prints_as_imported_enum_value(&self, expr: &Expr) -> bool {
match &expr.data {
ExprData::EDot(e) => {
e.optional_chain.is_none()
&& self
.try_to_get_imported_enum_value(e.target, &e.name)
.is_some()
}
ExprData::EIndex(e) => {
e.optional_chain.is_none()
&& e.index.data.as_e_string().is_some_and(|name| {
let name = name.flattened(self.bump);
name.is_utf8()
&& self
.try_to_get_imported_enum_value(e.target, name.slice8())
.is_some()
})
}
_ => false,
}
}

pub(crate) fn try_to_get_imported_enum_value(
&self,
target: Expr,
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 50: The body of a class with lowered class decorators names the class by the
/// binding that holds what the decorators return.
/// Version 51: A `bun:` specifier that names no builtin is printed as it is written.
const EXPECTED_VERSION: u32 = 51;
/// Version 52: A read through an `import X = A.B` alias of an enum or a namespace of the file folds.
const EXPECTED_VERSION: u32 = 52;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
Loading
Loading