perf(transformer_plugins): dispatch global defines by trailing name (#23666)

## Summary

`ReplaceGlobalDefines` matched every configured define against every AST node by linearly scanning the full define list — `O(nodes × defines)`. For each identifier reference it scanned all identifier defines; for each member expression it scanned all dot defines and all meta defines.

This keys defines by their **trailing segment** (the outermost property name) — the only segment a node can match on — so each node becomes a single hash lookup instead of a full scan:

- `identifier` defines → `FxHashMap<name, value>`
- `dot` defines (`process.env.NODE_ENV`) → `FxHashMap<last_part, Vec<DotDefine>>`
- non-wildcard `meta` defines (`import.meta.env.MODE`) → `FxHashMap<last_part, Vec<MetaPropertyDefine>>`
- wildcard `meta` defines (`import.meta.env.*`) match a *variable* trailing name, so they can't be keyed; they stay a small linear list, scanned only for `import.meta`-rooted expressions.

The config-time `sort` is removed — "specific wins over wildcard" is now structural (the map is consulted before the wildcard list). Computed members (`a["b"]`) extract the static key once and skip entirely when the key is dynamic.

## Correctness

`is_meta_property_define` now verifies `import` / `meta` at its `MetaProperty` arm, so `new.target.env` is no longer wrongly replaced by an `import.meta.env` define (previously it matched *any* meta property — `import.meta` **or** `new.target`). Added a regression test.

## Benchmark

Isolated `ReplaceGlobalDefines::build()` (re-parsing outside the timer) with a ~60-entry Vite-like define config:

| file | before | after | speedup |
|---|--:|--:|--:|
| RadixUIAdoptionSection.jsx | 2.38 µs | 0.64 µs | 3.7× |
| react.development.js | 72.2 µs | 39.0 µs | 1.9× |
| App.tsx | 852 µs | 307 µs | 2.8× |
| binder.ts | 243 µs | 88 µs | 2.8× |
| kitchen-sink.tsx | 1345 µs | 677 µs | 2.0× |

Speedup scales with the number of defines. With a single define the two are roughly even (an `FxHash` of a short property name ≈ the previous single leaf-compare), so no regression on small configs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-07-13 07:06:56 +00:00
1 parent 53342c91e9
commit 5b26643408
2 files changed
+316 -147

No files matched your search

@@ -1,6 +1,33 @@
use std::{cmp::Ordering, sync::Arc};
//! Replace global identifiers, member-expression chains, and `typeof` expressions with configured
//! constant values — the engine behind bundler `define` options (esbuild / Vite style).
//!
//! ```text
//! // define: `process.env.NODE_ENV` -> `"production"`, `__DEV__` -> `false`
//! if (process.env.NODE_ENV === "production" && !__DEV__) foo();
//! // becomes:
//! if ("production" === "production" && !false) foo();
//! ```
//!
//! The minifier then folds the now-constant condition, which is the point of `define`.
//!
//! # How matching is dispatched
//!
//! [`ReplaceGlobalDefines`] walks *every* expression in the AST, so checking each node against
//! every configured define would be `O(nodes * defines)`. We avoid that by keying defines on their
//! **trailing name**: a member expression `a.b.c` can only match a define whose last part is `c`,
//! and the visited node already carries that name (`member.property.name`). So the config buckets
//! defines into [`FxHashMap`]s keyed by that trailing segment, and each node becomes a single map
//! lookup; the full chain-walk verification only runs on the rare bucket hit. This makes the pass
//! effectively `O(nodes)`.
//!
//! Defines whose trailing name is not fixed (`import.meta.env.*`) or that apply only to one
//! operator (`typeof x`) cannot be keyed, so they fall back to small linearly-scanned lists — each
//! gated by a cheap check so ordinary code never touches them. The three storage tiers are laid out
//! on the config struct.
use rustc_hash::FxHashSet;
use std::sync::Arc;
use rustc_hash::{FxHashMap, FxHashSet};
use oxc_allocator::{Address, Allocator, ArenaBox, GetAddress, ReplaceWith, UnstableAddress};
use oxc_ast::ast::*;
@@ -30,20 +57,50 @@ static THIS_STR: Str<'static> = Str::new_const("this");
#[derive(Debug)]
struct IdentifierDefine {
identifier_defines: Vec<(/* key */ CompactStr, /* value */ CompactStr)>,
/// Identifier defines keyed by name, so each identifier reference is a single lookup instead
/// of a scan over every define.
identifier_defines: FxHashMap<CompactStr, CompactStr>,
/// Whether user want to replace `ThisExpression`, avoid linear scan for each `ThisExpression`
has_this_expr_define: bool,
}
/// Parsed, match-ready form of a [`ReplaceGlobalDefinesConfig`].
///
/// Defines are grouped into three tiers by how they are matched against AST nodes:
///
/// 1. **Keyed maps** (`identifier`, `dot`, `meta_property`) — bucketed by the trailing property
/// name, so matching a node is a single `O(1)` lookup. This is the common case and the reason
/// the whole pass is `O(nodes)` rather than `O(nodes * defines)`.
/// 2. **Linearly scanned lists** (`meta_property_wildcard`, `typeof_defines`) — defines whose
/// trailing name is variable (`import.meta.env.*`) or that only apply to a `typeof` operator, so
/// they cannot be keyed. Each scan is gated by a cheap check (is this an `import.meta` member? is
/// this a `typeof`?) so ordinary code never pays for them.
/// 3. **Special case** (`import_meta`) — the bare `import.meta` replacement.
#[derive(Debug)]
struct ReplaceGlobalDefinesConfigImpl {
// Tier 1 — keyed by trailing name, O(1) dispatch per node.
/// Identifier defines (e.g. `__DEV__`) keyed by name, plus whether `this` is replaced.
identifier: IdentifierDefine,
dot: Vec<DotDefine>,
/// Dot defines keyed by their last part (the outermost property name accessed), e.g.
/// `process.env.NODE_ENV` is bucketed under `NODE_ENV`. A member expression can only match a
/// define that shares its outermost property name, so keying on it avoids scanning unrelated
/// defines for every member expression in the file.
dot: FxHashMap<CompactStr, Vec<DotDefine>>,
/// Non-wildcard meta property defines, keyed by their last part (e.g. `import.meta.env.MODE`
/// under `MODE`, `import.meta.env` under `env`). Same rationale as [`Self::dot`].
meta_property: FxHashMap<CompactStr, Vec<MetaPropertyDefine>>,
// Tier 2 — scanned linearly, but gated so ordinary nodes skip the scan.
/// Wildcard meta property defines (`import.meta.env.*`, `import.meta.*`). These match a
/// variable trailing property name and so cannot be keyed by it; they are scanned linearly,
/// but only for `import.meta`-rooted expressions and there are normally very few of them.
meta_property_wildcard: Vec<MetaPropertyDefine>,
/// `typeof` defines (e.g. `typeof window` -> `"object"`). Scanned only when a `typeof` unary
/// expression is visited, so non-`typeof` code never touches them.
typeof_defines: Vec<TypeofDefine>,
meta_property: Vec<MetaPropertyDefine>,
/// extra field to avoid linear scan `meta_property` to check if it has `import.meta` every
/// time
/// Some(replacement): import.meta -> replacement
/// None -> no need to replace import.meta
// Tier 3 — special case.
/// Replacement for a bare `import.meta`. A dedicated field so we don't scan `meta_property` for
/// `import.meta` on every meta property. `Some` -> replace, `None` -> leave as is.
import_meta: Option<CompactStr>,
}
@@ -80,12 +137,15 @@ impl DotDefine {
}
}
/// What a config define key parses into. Produced by [`ReplaceGlobalDefinesConfig::check_key`] and
/// consumed by [`ReplaceGlobalDefinesConfig::new`] to route each define into the right storage tier.
enum IdentifierType {
Identifier,
DotDefines { parts: Vec<CompactStr> },
// `key` is the last part (the bucket key); `parts` is the full member path.
DotDefines { key: CompactStr, parts: Vec<CompactStr> },
Typeof { parts: Vec<CompactStr> },
// import.meta.a
ImportMetaWithParts { parts: Vec<CompactStr>, postfix_wildcard: bool },
// import.meta.a; `key` is the last part (used to bucket non-wildcard defines).
ImportMetaWithParts { key: CompactStr, parts: Vec<CompactStr>, postfix_wildcard: bool },
// import.meta or import.meta.*
ImportMeta(bool),
}
@@ -97,10 +157,12 @@ impl ReplaceGlobalDefinesConfig {
/// * value has a syntax error
pub fn new<S: AsRef<str>>(defines: &[(S, S)]) -> Result<Self, Diagnostics> {
let allocator = Allocator::default();
let mut identifier_defines = vec![];
let mut dot_defines = vec![];
let mut identifier_defines: FxHashMap<CompactStr, CompactStr> = FxHashMap::default();
let mut dot_defines: FxHashMap<CompactStr, Vec<DotDefine>> = FxHashMap::default();
let mut typeof_defines = vec![];
let mut meta_properties_defines = vec![];
let mut meta_property: FxHashMap<CompactStr, Vec<MetaPropertyDefine>> =
FxHashMap::default();
let mut meta_property_wildcard: Vec<MetaPropertyDefine> = vec![];
let mut import_meta = None;
let mut has_this_expr_define = false;
for (key, value) in defines {
@@ -112,24 +174,33 @@ impl ReplaceGlobalDefinesConfig {
match Self::check_key(key)? {
IdentifierType::Identifier => {
has_this_expr_define |= key == "this";
identifier_defines.push((CompactStr::new(key), CompactStr::new(value)));
// Keep the first definition for a duplicate key, matching the previous
// first-match-wins linear scan.
identifier_defines
.entry(CompactStr::new(key))
.or_insert_with(|| CompactStr::new(value));
}
IdentifierType::DotDefines { parts } => {
dot_defines.push(DotDefine::new(parts, CompactStr::new(value)));
IdentifierType::DotDefines { key, parts } => {
dot_defines
.entry(key)
.or_default()
.push(DotDefine::new(parts, CompactStr::new(value)));
}
IdentifierType::Typeof { parts } => {
typeof_defines.push(TypeofDefine { parts, value: CompactStr::new(value) });
}
IdentifierType::ImportMetaWithParts { parts, postfix_wildcard } => {
meta_properties_defines.push(MetaPropertyDefine::new(
parts,
CompactStr::new(value),
postfix_wildcard,
));
IdentifierType::ImportMetaWithParts { key, parts, postfix_wildcard } => {
let define =
MetaPropertyDefine::new(parts, CompactStr::new(value), postfix_wildcard);
if postfix_wildcard {
meta_property_wildcard.push(define);
} else {
meta_property.entry(key).or_default().push(define);
}
}
IdentifierType::ImportMeta(postfix_wildcard) => {
if postfix_wildcard {
meta_properties_defines.push(MetaPropertyDefine::new(
meta_property_wildcard.push(MetaPropertyDefine::new(
vec![],
CompactStr::new(value),
postfix_wildcard,
@@ -140,23 +211,15 @@ impl ReplaceGlobalDefinesConfig {
}
}
}
// Always move specific meta define before wildcard dot define
// Keep other order unchanged
// see test case replace_global_definitions_dot_with_postfix_mixed as an example
meta_properties_defines.sort_by(|a, b| {
if !a.postfix_wildcard && b.postfix_wildcard {
Ordering::Less
} else if a.postfix_wildcard && b.postfix_wildcard {
Ordering::Greater
} else {
Ordering::Equal
}
});
// No sort needed: at match time the non-wildcard `meta_property` map is always consulted
// before the `meta_property_wildcard` list, which preserves "specific wins over wildcard".
// See test case `dot_with_postfix_mixed`.
Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl {
identifier: IdentifierDefine { identifier_defines, has_this_expr_define },
dot: dot_defines,
meta_property,
meta_property_wildcard,
typeof_defines,
meta_property: meta_properties_defines,
import_meta,
})))
}
@@ -187,10 +250,14 @@ impl ReplaceGlobalDefinesConfig {
let is_import_meta = parts[0] == "import" && parts[1] == "meta";
let compact_parts = Self::check_identifier_parts(key, &parts[0..normalized_parts_len])?;
// The last non-wildcard part is the bucket key (the outermost property name accessed).
// It always exists here: `normalized_parts_len >= 2` after the `parts.len() == 1` return.
let key = CompactStr::new(parts[normalized_parts_len - 1]);
if is_import_meta {
match normalized_parts_len {
2 => Ok(IdentifierType::ImportMeta(normalized_parts_len != parts.len())),
_ => Ok(IdentifierType::ImportMetaWithParts {
key,
parts: compact_parts.into_iter().skip(2).collect(),
postfix_wildcard: normalized_parts_len != parts.len(),
}),
@@ -202,7 +269,7 @@ impl ReplaceGlobalDefinesConfig {
)]
.into())
} else {
Ok(IdentifierType::DotDefines { parts: compact_parts })
Ok(IdentifierType::DotDefines { key, parts: compact_parts })
}
}
@@ -423,6 +490,10 @@ impl<'a> ReplaceGlobalDefines<'a> {
expr
}
// ===== Matchers =====
// One entry point per define kind. `visit_expression` tries them in the order
// typeof -> identifier -> dot/meta; the first one to match replaces the node.
fn replace_identifier_defines(&mut self, expr: &mut Expression<'a>) -> bool {
match expr {
Expression::Identifier(ident) => {
@@ -435,14 +506,11 @@ impl<'a> ReplaceGlobalDefines<'a> {
if self.config.0.identifier.has_this_expr_define
&& should_replace_this_expr(self.current_scope_flags()) =>
{
for (key, value) in &self.config.0.identifier.identifier_defines {
if key.as_str() == "this" {
let value = value.clone();
let value = self.parse_value(&value);
*expr = value;
return true;
}
if let Some(value) = self.config.0.identifier.identifier_defines.get("this") {
let value = value.clone();
let value = self.parse_value(&value);
*expr = value;
return true;
}
}
_ => {}
@@ -519,8 +587,7 @@ impl<'a> ReplaceGlobalDefines<'a> {
return define.parts.len() == 2
&& define.parts[0].as_str() == "import"
&& define.parts[1].as_str() == "meta"
&& meta.meta.name == "import"
&& meta.property.name == "meta";
&& is_import_meta(meta);
}
_ => return false,
};
@@ -534,14 +601,9 @@ impl<'a> ReplaceGlobalDefines<'a> {
if !Self::is_global_or_ambient_reference(self.scoping(), ident) {
return None;
}
for (key, value) in &self.config.0.identifier.identifier_defines {
if ident.name.as_str() == key {
let value = value.clone();
let value = self.parse_value(&value);
return Some(value);
}
}
None
let value = self.config.0.identifier.identifier_defines.get(ident.name.as_str())?.clone();
let value = self.parse_value(&value);
Some(value)
}
fn replace_define_with_assignment_expr(&mut self, node: &mut AssignmentExpression<'a>) -> bool {
@@ -601,8 +663,7 @@ impl<'a> ReplaceGlobalDefines<'a> {
}
Expression::MetaProperty(meta_property) => {
if let Some(replacement) = self.config.0.import_meta.clone()
&& meta_property.meta.name == "import"
&& meta_property.property.name == "meta"
&& is_import_meta(meta_property)
{
let value = self.parse_value(&replacement);
*expr = value;
@@ -618,68 +679,23 @@ impl<'a> ReplaceGlobalDefines<'a> {
&mut self,
member: &ComputedMemberExpression<'a>,
) -> Option<Expression<'a>> {
let scoping = self.scoping();
let scope_flags = self.current_scope_flags();
let value = self
.config
.0
.dot
.iter()
.find(|dot_define| {
Self::is_dot_define(
scoping,
scope_flags,
dot_define,
DotDefineMemberExpression::ComputedMemberExpression(member),
)
})
.map(|dot_define| dot_define.value.clone());
if let Some(value) = value {
let value = self.parse_value(&value);
return Some(value);
}
// Only a static computed key (e.g. `a["b"]`) can match a define; a dynamic key never does,
// and its outermost property name is what selects the candidate defines.
let leaf = static_property_name_of_computed_expr(member)?;
// TODO: meta_property_define
None
let value = self.find_dot_define_value(
leaf.as_str(),
DotDefineMemberExpression::ComputedMemberExpression(member),
)?;
Some(self.parse_value(&value))
}
fn replace_dot_static_member_expr(
&mut self,
member: &StaticMemberExpression<'a>,
) -> Option<Expression<'a>> {
let scoping = self.scoping();
let scope_flags = self.current_scope_flags();
let value = self
.config
.0
.dot
.iter()
.find(|dot_define| {
Self::is_dot_define(
scoping,
scope_flags,
dot_define,
DotDefineMemberExpression::StaticMemberExpression(member),
)
})
.map(|dot_define| dot_define.value.clone());
if let Some(value) = value {
let value = self.parse_value(&value);
return Some(self.destructing_dot_define_optimizer(value));
}
let value = self
.config
.0
.meta_property
.iter()
.find(|meta_property_define| {
Self::is_meta_property_define(meta_property_define, member)
})
.map(|meta_property_define| meta_property_define.value.clone());
if let Some(value) = value {
let value = self.parse_value(&value);
return Some(self.destructing_dot_define_optimizer(value));
}
None
let value = self.replace_dot_static_member_expr_impl(member)?;
Some(self.destructing_dot_define_optimizer(value))
}
/// Like `replace_dot_static_member_expr` but without the destructuring optimization.
@@ -688,42 +704,77 @@ impl<'a> ReplaceGlobalDefines<'a> {
&mut self,
member: &StaticMemberExpression<'a>,
) -> Option<Expression<'a>> {
self.replace_dot_static_member_expr_impl(member)
}
/// Shared lookup for a static member expression: a `dot` define, then a `meta_property`
/// define. Returns the parsed replacement expression, or `None` if nothing matches.
fn replace_dot_static_member_expr_impl(
&mut self,
member: &StaticMemberExpression<'a>,
) -> Option<Expression<'a>> {
let leaf = member.property.name.as_str();
let value = self
.find_dot_define_value(leaf, DotDefineMemberExpression::StaticMemberExpression(member))
.or_else(|| self.find_meta_property_value(leaf, member))?;
Some(self.parse_value(&value))
}
/// Find the value of a `dot` define matching `member`, whose outermost property name is `leaf`.
/// Only defines bucketed under `leaf` can match, so this is a single map lookup instead of a
/// scan over every define.
fn find_dot_define_value(
&self,
leaf: &str,
member: DotDefineMemberExpression<'_, 'a>,
) -> Option<CompactStr> {
let bucket = self.config.0.dot.get(leaf)?;
let scoping = self.scoping();
let scope_flags = self.current_scope_flags();
let value = self
.config
.0
.dot
bucket
.iter()
.find(|dot_define| {
Self::is_dot_define(
scoping,
scope_flags,
dot_define,
DotDefineMemberExpression::StaticMemberExpression(member),
)
})
.map(|dot_define| dot_define.value.clone());
if let Some(value) = value {
let value = self.parse_value(&value);
return Some(value);
.find(|dot_define| Self::is_dot_define(scoping, scope_flags, dot_define, member))
.map(|dot_define| dot_define.value.clone())
}
/// Find the value of a meta property define matching the static member `member`, whose
/// outermost property name is `leaf`. Non-wildcard defines are bucketed under `leaf`; wildcard
/// defines match a variable trailing name, so they are scanned linearly but only for
/// `import.meta`-rooted expressions.
fn find_meta_property_value(
&self,
leaf: &str,
member: &StaticMemberExpression<'a>,
) -> Option<CompactStr> {
if let Some(bucket) = self.config.0.meta_property.get(leaf)
&& let Some(define) =
bucket.iter().find(|define| Self::is_meta_property_define(define, member))
{
return Some(define.value.clone());
}
let value = self
.config
.0
.meta_property
.iter()
.find(|meta_property_define| {
Self::is_meta_property_define(meta_property_define, member)
})
.map(|meta_property_define| meta_property_define.value.clone());
if let Some(value) = value {
let value = self.parse_value(&value);
return Some(value);
if !self.config.0.meta_property_wildcard.is_empty() && Self::is_import_meta_member(member) {
return self
.config
.0
.meta_property_wildcard
.iter()
.find(|define| Self::is_meta_property_define(define, member))
.map(|define| define.value.clone());
}
None
}
/// Whether `member`'s object chain roots at `import.meta`.
///
/// This is a cheap fast-reject, not a correctness check ([`Self::is_meta_property_define`]
/// already rejects non-`import.meta` roots): a meta property define can only ever match a
/// member expression rooted at `import.meta`, so walking the chain once here lets the caller
/// skip the whole wildcard scan for ordinary member expressions such as `console.log` or
/// `a.b.c`.
fn is_import_meta_member(member: &StaticMemberExpression<'a>) -> bool {
matches!(member.get_first_object(), Expression::MetaProperty(meta) if is_import_meta(meta))
}
pub fn is_meta_property_define(
meta_define: &MetaPropertyDefine,
member: &StaticMemberExpression<'a>,
@@ -735,9 +786,7 @@ impl<'a> ReplaceGlobalDefines<'a> {
}
if meta_define.parts.is_empty() && meta_define.postfix_wildcard {
match &member.object {
Expression::MetaProperty(meta) => {
return meta.meta.name == "import" && meta.property.name == "meta";
}
Expression::MetaProperty(meta) => return is_import_meta(meta),
_ => return false,
}
}
@@ -783,7 +832,11 @@ impl<'a> ReplaceGlobalDefines<'a> {
cur_part_name = &member.property.name;
Some(member)
}
Expression::MetaProperty(_) => {
Expression::MetaProperty(meta) => {
// Only `import.meta` is a valid root; never `new.target`.
if !is_import_meta(meta) {
return false;
}
if meta_define.postfix_wildcard {
// `import.meta.env` should not match `import.meta.env.*`
return has_matched_part && !is_full_match;
@@ -942,7 +995,7 @@ impl<'a> ReplaceGlobalDefines<'a> {
// Handle import.meta
// When we encounter a MetaProperty, we need to verify that the remaining
// parts match ["import", "meta"]
if meta.meta.name == "import" && meta.property.name == "meta" {
if is_import_meta(meta) {
// At this point, i is the current position we're checking
// We need the next two parts (going backwards) to be "meta" then "import"
// i.e., parts[i-1] == "meta" and parts[i-2] == "import"
@@ -1071,6 +1124,11 @@ const fn should_replace_this_expr(scope_flags: ScopeFlags) -> bool {
&& (!scope_flags.contains(ScopeFlags::Function) || scope_flags.contains(ScopeFlags::Arrow))
}
/// Whether `meta` is the `import.meta` meta property (as opposed to `new.target`).
fn is_import_meta(meta: &MetaProperty) -> bool {
meta.meta.name == "import" && meta.property.name == "meta"
}
fn assignment_target_from_expr(expr: Expression) -> Option<AssignmentTarget> {
match expr {
Expression::ComputedMemberExpression(expr) => {
@@ -280,6 +280,117 @@ fn dot_with_postfix_mixed() {
test("foo(import.meta)", "foo(1)", &config);
}
#[test]
fn meta_property_not_new_target() {
// `new.target` is also a `MetaProperty`, but an `import.meta.*` define must only match
// `import.meta`, never `new.target`.
let config = config(&[("import.meta.env", "__foo__"), ("import.meta.env.*", "undefined")]);
test(
"export function f() { return new.target.env; }",
"export function f() { return new.target.env; }",
&config,
);
test(
"export function f() { return new.target.env.FOO; }",
"export function f() { return new.target.env.FOO; }",
&config,
);
// sanity: the same config still rewrites the real `import.meta`
test("const _ = import.meta.env", "__foo__", &config);
}
// ---- adversarial: cases exercising the trailing-name buckets (this PR's data structure) ----
#[test]
fn bucket_multiple_dot_defines() {
// Two distinct chains share the trailing name `X` -> one bucket, len > 1.
// `find` must pick the entry whose full chain matches, not just the first in the bucket.
let c = config(&[("a.b.X", "1"), ("c.d.X", "2")]);
test("foo(a.b.X)", "foo(1)", &c);
test("foo(c.d.X)", "foo(2)", &c);
test_same("foo(e.f.X)", &c);
// Shorter vs longer chain in the same bucket.
let c = config(&[("b.X", "10"), ("a.b.X", "20")]);
test("foo(b.X)", "foo(10)", &c);
test("foo(a.b.X)", "foo(20)", &c);
}
#[test]
fn bucket_multiple_meta_defines() {
// Two meta defines share the trailing name `X`.
let config = config(&[("import.meta.a.X", "1"), ("import.meta.b.X", "2")]);
test("foo(import.meta.a.X)", "foo(1)", &config);
test("foo(import.meta.b.X)", "foo(2)", &config);
test_same("foo(import.meta.c.X)", &config);
}
#[test]
fn dot_and_meta_share_trailing_name() {
// A dot define and a meta define bucket under the same name `env`.
// The dot map is consulted first, then the meta map; neither must steal the other's match.
let config = config(&[("app.env", "1"), ("import.meta.env", "2")]);
test("foo(app.env)", "foo(1)", &config);
test("foo(import.meta.env)", "foo(2)", &config);
test_same("foo(other.env)", &config);
}
#[test]
fn duplicate_keys_keep_first() {
// Identifier duplicate.
let c = config(&[("DUP", "1"), ("DUP", "2")]);
test("foo(DUP)", "foo(1)", &c);
// Dot duplicate (same chain twice -> two entries in one bucket).
let c = config(&[("a.b.DUP", "1"), ("a.b.DUP", "2")]);
test("foo(a.b.DUP)", "foo(1)", &c);
}
#[test]
fn specific_meta_beats_wildcard_regardless_of_config_order() {
// Wildcard listed BEFORE the specific define; the specific must still win, because the
// keyed map is always consulted before the wildcard list (the old sort is gone).
let config = config(&[("import.meta.env.*", "0"), ("import.meta.env.MODE", "1")]);
test("foo(import.meta.env.MODE)", "foo(1)", &config);
test("foo(import.meta.env.OTHER)", "foo(0)", &config);
}
#[test]
fn computed_key_dispatches_to_bucket() {
let config = config(&[("a.b.C", "1")]);
test("foo(a.b['C'])", "foo(1)", &config);
test("foo(a['b'].C)", "foo(1)", &config);
// Single-quasi template literal key resolves to a static name too.
test("foo(a.b[`C`])", "foo(1)", &config);
// Dynamic key can never match a define.
test_same("foo(a.b[C])", &config);
}
#[test]
fn global_meta_dot_define_does_not_match_import_meta() {
// A dot define rooted at a global `meta` shares the bucket key `X` with nothing else, but its
// chain walks onto a `MetaProperty`. The `import.meta` guard must keep `import.meta.X` from
// matching a `meta.X` define meant for a global identifier named `meta`.
let c = config(&[("meta.X", "1")]);
test("foo(meta.X)", "foo(1)", &c);
test_same("foo(import.meta.X)", &c);
}
#[test]
fn optional_chain_dispatches_to_bucket() {
// Optional chaining must still resolve to the right bucket entry.
let c = config(&[("a.b.X", "1"), ("c.d.X", "2")]);
test("foo(a?.b.X)", "foo(1)", &c);
test("foo(c.d?.X)", "foo(2)", &c);
}
#[test]
fn assignment_target_dispatches_to_bucket() {
// The no-optimize assignment path shares the same bucket lookup.
let c = config(&[("a.b.X", "lhs1"), ("c.d.X", "lhs2")]);
test("a.b.X = 0", "lhs1 = 0", &c);
test("c.d.X = 0", "lhs2 = 0", &c);
}
#[test]
fn optional_chain() {
let config = config(&[("a.b.c", "1"), ("process.env", "{}")]);