mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
fix(transformer): replace ambient dot defines (#23231)
## Summary Dot define replacement now treats ambient TypeScript declarations as replaceable global roots. This fixes cases like service worker code that declares `self` for type checking while still expecting `self.__WB_MANIFEST` to be replaced. Before this change, dot defines only matched when the root identifier was unresolved/global: ```ts foo.bar ``` would replace only if `foo` had no local symbol. In the bug case: ```ts declare let self: ServiceWorkerGlobalScope; precacheAndRoute(self.__WB_MANIFEST); ``` semantic analysis resolves `self` to the local `declare let self` symbol, so `ident.is_global_reference(scoping)` returned `false` and `self.__WB_MANIFEST` was treated as shadowed. The new helper still allows true globals, but also allows symbols marked ambient: ```rust .is_none_or(|symbol_id| scoping.symbol_flags(symbol_id).is_ambient()) ``` That means: - unresolved `self` -> replace - `declare let self` -> replace, because it is TypeScript-only and not a runtime binding - `let self` or function parameter `self` -> do not replace, because those are real runtime shadows So this fixes the bug without making dot defines scope-insensitive. Fixes #23224
This commit is contained in:
1 parent
79cd905cd5
commit
52d0c31ed4
2 files changed
+62
-13
No files matched your search
@@ -7,7 +7,7 @@ use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::{VisitMut, walk_mut};
|
||||
use oxc_diagnostics::{Diagnostics, OxcDiagnostic};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_semantic::{IsGlobalReference, ReferenceFlags, ScopeFlags, Scoping};
|
||||
use oxc_semantic::{ReferenceFlags, ScopeFlags, Scoping};
|
||||
use oxc_span::{SPAN, SourceType};
|
||||
use oxc_str::CompactStr;
|
||||
use oxc_syntax::identifier::is_identifier_name;
|
||||
@@ -399,18 +399,9 @@ impl<'a> ReplaceGlobalDefines<'a> {
|
||||
&mut self,
|
||||
ident: &oxc_allocator::Box<'_, IdentifierReference<'_>>,
|
||||
) -> Option<Expression<'a>> {
|
||||
let scoping = self.scoping();
|
||||
if let Some(symbol_id) = ident
|
||||
.reference_id
|
||||
.get()
|
||||
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
|
||||
{
|
||||
// Ignore `declare const IS_PROD: boolean;`
|
||||
if !scoping.symbol_flags(symbol_id).is_ambient() {
|
||||
return None;
|
||||
}
|
||||
if !Self::is_global_or_ambient_reference(self.scoping(), ident) {
|
||||
return None;
|
||||
}
|
||||
// This is a global variable, including ambient variants such as `declare const`.
|
||||
for (key, value) in &self.config.0.identifier.identifier_defines {
|
||||
if ident.name.as_str() == key {
|
||||
let value = value.clone();
|
||||
@@ -789,7 +780,7 @@ impl<'a> ReplaceGlobalDefines<'a> {
|
||||
})
|
||||
}
|
||||
Expression::Identifier(ident) => {
|
||||
if !ident.is_global_reference(scoping) {
|
||||
if !Self::is_global_or_ambient_reference(scoping, ident) {
|
||||
return false;
|
||||
}
|
||||
cur_part_name = &ident.name;
|
||||
@@ -863,6 +854,30 @@ impl<'a> ReplaceGlobalDefines<'a> {
|
||||
obj.properties.retain(|_| *iter.next().unwrap());
|
||||
expr
|
||||
}
|
||||
|
||||
/// Return whether an identifier reference should be treated like a global define root.
|
||||
///
|
||||
/// Unresolved references are globals, so `foo.bar` can be replaced by a `foo.bar` define.
|
||||
/// Ambient declarations are also replaceable because they are TypeScript-only and do not
|
||||
/// create runtime bindings:
|
||||
///
|
||||
/// ```ts
|
||||
/// declare let self: ServiceWorkerGlobalScope;
|
||||
/// precacheAndRoute(self.__WB_MANIFEST); // replace `self.__WB_MANIFEST`
|
||||
/// ```
|
||||
///
|
||||
/// Runtime bindings still shadow define roots:
|
||||
///
|
||||
/// ```ts
|
||||
/// let self;
|
||||
/// precacheAndRoute(self.__WB_MANIFEST); // do not replace
|
||||
/// ```
|
||||
fn is_global_or_ambient_reference(scoping: &Scoping, ident: &IdentifierReference<'_>) -> bool {
|
||||
scoping
|
||||
.get_reference(ident.reference_id())
|
||||
.symbol_id()
|
||||
.is_none_or(|symbol_id| scoping.symbol_flags(symbol_id).is_ambient())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
||||
@@ -36,6 +36,25 @@ pub fn test(source_text: &str, expected: &str, config: &ReplaceGlobalDefinesConf
|
||||
assert_eq!(result, expected, "for source {source_text}");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_define_only(source_text: &str, expected: &str, config: &ReplaceGlobalDefinesConfig) {
|
||||
let source_type = SourceType::ts().with_module(true);
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
||||
assert!(ret.diagnostics.is_empty());
|
||||
let mut program = ret.program;
|
||||
let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
|
||||
let ret = ReplaceGlobalDefines::new(&allocator, config.clone()).build(scoping, &mut program);
|
||||
assert_eq!(ret.changed, source_text != expected);
|
||||
AssertAst.visit_program(&program);
|
||||
let result = Codegen::new()
|
||||
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
|
||||
.build(&program)
|
||||
.code;
|
||||
let expected = codegen(expected, source_type);
|
||||
assert_eq!(result, expected, "for source {source_text}");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_same(source_text: &str, config: &ReplaceGlobalDefinesConfig) {
|
||||
test(source_text, source_text, config);
|
||||
@@ -282,6 +301,21 @@ fn declare_const() {
|
||||
test("declare const IS_PROD: boolean; if (IS_PROD) {} foo(IS_PROD)", "foo(true)", &config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declare_dot_define() {
|
||||
let config = config(&[("process.env.NODE_ENV", "'production'")]);
|
||||
test_define_then_transform_ts(
|
||||
"declare let process: { env: { NODE_ENV: string } }; foo(process.env.NODE_ENV)",
|
||||
"foo('production')",
|
||||
&config,
|
||||
);
|
||||
test_define_only(
|
||||
"declare let process: { env: { NODE_ENV: string } }; foo(process.env.NODE_ENV)",
|
||||
"declare let process: { env: { NODE_ENV: string } }; foo('production')",
|
||||
&config,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn test_sourcemap() {
|
||||
|
||||
Reference in new issue
Block a user