Skip to content
Open
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
73 changes: 43 additions & 30 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7183,14 +7183,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
match expr.data {
js_ast::ExprData::EDot(ex) => {
if parts.len() > 1 {
if ex.optional_chain.is_some() {
return false;
}
// Intermediates must be dot expressions
let last = parts.len() - 1;
let is_tail_match = strings::eql(&parts[last], &ex.name);
return is_tail_match && self.is_dot_define_match(ex.target, &parts[..last]);
}

// Allow globalThis.X to match a define for X (e.g. globalThis.process.env.NODE_ENV)
if parts.len() == 1 && strings::eql(&parts[0], &ex.name) {
return self.is_global_this(ex.target);
}
}
js_ast::ExprData::EImportMeta(_) => {
return parts.len() == 2 && &*parts[0] == b"import" && &*parts[1] == b"meta";
Expand All @@ -7200,49 +7202,60 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// we do, but only if it's a UTF8 string
// the intent is to handle people using this form instead of E.Dot. So we really only want to do this if the accessor can also be an identifier
js_ast::ExprData::EIndex(index) => {
if parts.len() > 1 {
if let js_ast::ExprData::EString(mut s) = index.index.data {
if s.is_utf8() {
if index.optional_chain.is_some() {
return false;
}
if let js_ast::ExprData::EString(mut s) = index.index.data {
if s.is_utf8() {
if parts.len() > 1 {
let last = parts.len() - 1;
let is_tail_match = strings::eql(&parts[last], s.slice(self.arena));
return is_tail_match
&& self.is_dot_define_match(index.target, &parts[..last]);
}

// Allow globalThis["X"] to match a define for X
if parts.len() == 1 && strings::eql(&parts[0], s.slice(self.arena)) {
return self.is_global_this(index.target);
}
}
}
}
js_ast::ExprData::EIdentifier(ex) => {
js_ast::ExprData::EIdentifier(_) => {
// The last expression must be an identifier
if parts.len() == 1 {
let name = self.load_name_from_ref(ex.ref_);
if !strings::eql(name, &parts[0]) {
return false;
}

let Ok(result) = self.find_symbol_with_record_usage::<false>(expr.loc, name)
else {
return false;
};

// We must not be in a "with" statement scope
if result.is_inside_with_scope {
return false;
}

// when there's actually no symbol by that name, we return Ref.None
// If a symbol had already existed by that name, we return .unbound
return result.r#ref.is_empty()
|| self.symbols[result.r#ref.inner_index() as usize].kind
== js_ast::symbol::Kind::Unbound;
return self.is_unbound_identifier_named(expr, &parts[0]);
}
}
_ => {}
}
false
}

/// `expr` is the identifier `name`, unshadowed and not inside a `with`.
fn is_unbound_identifier_named(&mut self, expr: Expr, name: &[u8]) -> bool {
let js_ast::ExprData::EIdentifier(ex) = expr.data else {
return false;
};
let ident_name = self.load_name_from_ref(ex.ref_);
if !strings::eql(ident_name, name) {
return false;
}

let Ok(result) = self.find_symbol_with_record_usage::<false>(expr.loc, ident_name) else {
return false;
};

if result.is_inside_with_scope {
return false;
}

// No symbol by that name yields Ref::None; a pre-existing one is Unbound.
result.r#ref.is_empty()
|| self.symbols[result.r#ref.inner_index() as usize].kind
== js_ast::symbol::Kind::Unbound
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn is_global_this(&mut self, expr: Expr) -> bool {
self.is_unbound_identifier_named(expr, b"globalThis")
}
}

/// The unscoped npm package of a specifier (`react/x`) or path (`node_modules<sep>react<sep>x.js`).
Expand Down
52 changes: 30 additions & 22 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1354,29 +1354,32 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// while iterating without laundering.
let defines = p.define;
if let Some(parts) = defines.dots_for(e_.name.slice()) {
let mut best_value: Option<&crate::DefineData> = None;
let mut best_value_len: usize = 0;
let mut best_drop_len: usize = 0;
for define in parts.as_slice() {
if p.is_dot_define_match(expr, &define.parts) {
if in_.assign_target == js_ast::AssignTarget::None {
// Substitute user-specified defines
if !define.data.valueless() {
*e = p.value_for_define(
expr.loc,
in_.assign_target,
is_delete_target,
&define.data,
);
return;
}
if !p.is_dot_define_match(expr, &define.parts) {
continue;
}

if define.data.method_call_must_be_replaced_with_undefined()
&& in_
.property_access_for_method_call_maybe_should_replace_with_undefined
{
p.method_call_must_be_replaced_with_undefined = true;
}
}
if in_.assign_target == js_ast::AssignTarget::None
&& !define.data.valueless()
&& define.parts.len() >= best_value_len
{
best_value = Some(&define.data);
best_value_len = define.parts.len();
}

// Copy the side effect flags over in case this expression is unused
if in_.assign_target == js_ast::AssignTarget::None
&& define.data.method_call_must_be_replaced_with_undefined()
&& in_.property_access_for_method_call_maybe_should_replace_with_undefined
&& define.parts.len() >= best_drop_len
{
best_drop_len = define.parts.len();
}

// `a?.b` short-circuits observably, so `Symbol?.for(...)` is not pure.
if e_.optional_chain.is_none() {
if define.data.can_be_removed_if_unused() {
e_.can_be_removed_if_unused = true;
}
Expand All @@ -1387,10 +1390,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
e_.call_can_be_unwrapped_if_unused =
define.data.call_can_be_unwrapped_if_unused();
}

break;
}
}

if best_drop_len > best_value_len {
p.method_call_must_be_replaced_with_undefined = true;
} else if let Some(data) = best_value {
*e = p.value_for_define(expr.loc, in_.assign_target, is_delete_target, data);
return;
}
}

// Track ".then().catch()" chains
Expand Down
95 changes: 90 additions & 5 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ describe("bundler", () => {
},
});
itBundled("edgecase/NodeEnvOptionalChaining", {
// Matching `process?.env?.NODE_ENV` against the `process.env.NODE_ENV`
// define would also match `Symbol?.for` etc. as side-effect-free; esbuild
// bails on optional-chain links for the same reason.
todo: true,
files: {
"/entry.js": /* js */ `
capture(process?.env?.NODE_ENV);
Expand All @@ -187,14 +183,103 @@ describe("bundler", () => {
capture(process?.env.NODE_ENV);
capture(process?.env.NODE_ENV === 'production');
capture(process?.env.NODE_ENV === 'development');
capture(globalThis.process.env.NODE_ENV);
capture(globalThis.process.env.NODE_ENV === 'production');
capture(globalThis.process.env.NODE_ENV === 'development');
capture(globalThis.process?.env?.NODE_ENV);
capture(globalThis.process?.env?.NODE_ENV === 'production');
capture(globalThis.process?.env?.NODE_ENV === 'development');
Comment thread
robobun marked this conversation as resolved.
capture(globalThis["process"].env.NODE_ENV);
capture(globalThis["process"].env.NODE_ENV === 'production');
capture(globalThis["process"].env.NODE_ENV === 'development');
`,
},
target: "browser",
capture: ['"development"', "false", "true", '"development"', "false", "true", '"development"', "false", "true"],
capture: [
'"development"',
"false",
"true",
'"development"',
"false",
"true",
'"development"',
"false",
"true",
'"development"',
"false",
"true",
'"development"',
"false",
"true",
'"development"',
"false",
"true",
],
env: {
NODE_ENV: "development",
},
Comment thread
robobun marked this conversation as resolved.
});
// Regression for the globalThis base case added to isDotDefineMatch: an explicit
// `--define:globalThis.X.Y` must not be shadowed by a built-in valueless define
// for `["X","Y"]` (e.g. `Math.PI`) when matching a `globalThis.X.Y` expression.
itBundled("edgecase/NodeEnvDefineOverridesBuiltinThroughGlobalThis", {
files: {
"/entry.js": /* js */ `
capture(globalThis.Math.PI);
`,
},
target: "browser",
define: { "globalThis.Math.PI": "3" },
capture: ["3"],
});
// When both `X.Y` and `globalThis.X.Y` are defined, a `globalThis.X.Y` expression
// must resolve to the more specific `globalThis.X.Y` value regardless of the
// hash-map iteration order of the two user defines.
itBundled("edgecase/NodeEnvMoreSpecificGlobalThisDefineWins", {
files: {
"/entry.js": /* js */ `
capture(globalThis.FOO.BAR);
capture(FOO.BAR);
`,
},
target: "browser",
define: {
"FOO.BAR": '"bare"',
"globalThis.FOO.BAR": '"via_globalThis"',
},
capture: ['"via_globalThis"', '"bare"'],
});
// A more-specific `--define:globalThis.X.Y` must win over a less-specific
// `--drop=X.Y` for a `globalThis.X.Y(...)` call: the substituted identifier
// should be emitted, not dropped to `undefined`.
itBundled("edgecase/NodeEnvDefineBeatsDropAcrossGlobalThis", {
files: {
"/entry.js": /* js */ `
capture(globalThis.console.log("x"));
`,
},
target: "browser",
drop: ["console.log"],
define: {
"globalThis.console.log": "myLogger",
},
capture: ['myLogger("x")'],
});
// The symmetric inverse: a more-specific `--drop=globalThis.X.Y` must win over
// a less-specific `--define:X.Y=v` for `globalThis.X.Y(...)`.
itBundled("edgecase/NodeEnvDropBeatsDefineAcrossGlobalThis", {
files: {
"/entry.js": /* js */ `
capture(globalThis.console.log("x"));
`,
},
target: "browser",
drop: ["globalThis.console.log"],
define: {
"console.log": "myLogger",
},
capture: ["undefined"],
});

itBundled("edgecase/StarExternal", {
files: {
Expand Down
Loading