use oxc::ast::ast::{
ArrowFunctionExpression, CallExpression, ExportNamedDeclaration, Expression, Function,
IdentifierReference, Statement, TSType,
};
use oxc::ast_visit::{Visit, walk};
use oxc::semantic::ScopeFlags;
use rolldown_common::AstScopes;
pub struct TopLevelImportReadDetector<'scopes> {
scopes: &'scopes AstScopes,
reads_import: bool,
}
impl<'scopes> TopLevelImportReadDetector<'scopes> {
pub fn detect(scopes: &'scopes AstScopes, stmt: &Statement) -> bool {
let mut detector = Self { scopes, reads_import: false };
detector.visit_statement(stmt);
detector.reads_import
}
fn visit_immediately_invoked_function(&mut self, callee: &Expression<'_>) {
match callee.get_inner_expression() {
Expression::FunctionExpression(function) => {
self.visit_formal_parameters(&function.params);
if !function.generator
&& let Some(body) = &function.body
{
self.visit_function_body(body);
}
}
Expression::ArrowFunctionExpression(arrow) => {
self.visit_formal_parameters(&arrow.params);
self.visit_function_body(&arrow.body);
}
Expression::SequenceExpression(sequence) => {
if let Some(last) = sequence.expressions.last() {
self.visit_immediately_invoked_function(last);
}
}
_ => {}
}
}
}
impl<'ast> Visit<'ast> for TopLevelImportReadDetector<'_> {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'ast>) {
if self.reads_import {
return;
}
let Some(reference_id) = it.reference_id.get() else {
return;
};
let Some(symbol_id) = self.scopes.symbol_id_for(reference_id) else {
return;
};
if self.scopes.scoping().symbol_flags(symbol_id).is_import() {
self.reads_import = true;
}
}
fn visit_expression(&mut self, it: &Expression<'ast>) {
if self.reads_import {
return;
}
walk::walk_expression(self, it);
}
fn visit_function(&mut self, _it: &Function<'ast>, _flags: ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _it: &ArrowFunctionExpression<'ast>) {}
fn visit_call_expression(&mut self, it: &CallExpression<'ast>) {
if self.reads_import {
return;
}
walk::walk_call_expression(self, it);
if !self.reads_import {
self.visit_immediately_invoked_function(&it.callee);
}
}
fn visit_export_named_declaration(&mut self, it: &ExportNamedDeclaration<'ast>) {
if let Some(declaration) = &it.declaration {
self.visit_declaration(declaration);
}
}
fn visit_ts_type(&mut self, _it: &TSType<'ast>) {}
}
#[cfg(test)]
mod test {
use super::TopLevelImportReadDetector;
use oxc::span::SourceType;
use rolldown_common::AstScopes;
use rolldown_ecmascript::{EcmaAst, EcmaCompiler};
fn detect(code: &str) -> Vec<bool> {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
ast
.program()
.body
.iter()
.map(|stmt| TopLevelImportReadDetector::detect(&ast_scopes, stmt))
.collect()
}
#[test]
fn direct_and_member_reads() {
assert_eq!(
detect("import { counter } from './state'; export const snap = counter;"),
vec![false, true]
);
assert_eq!(
detect("import { obj } from './state'; export const snap = obj.value;"),
vec![false, true]
);
assert_eq!(detect("import { x } from './state'; sideEffect(x);"), vec![false, true]);
assert_eq!(
detect("import { x } from './state'; export const o = { a: x };"),
vec![false, true]
);
}
#[test]
fn nested_computed_key_hole_is_closed() {
assert_eq!(
detect("import { key } from './state'; const obj = {}; export const snap = obj[key].y;"),
vec![false, false, true]
);
assert_eq!(
detect("import { key } from './state'; const obj = {}; export const snap = obj[key][key];"),
vec![false, false, true]
);
}
#[test]
fn namespace_member_and_alias() {
assert_eq!(
detect("import * as ns from './state'; export const snap = ns.foo;"),
vec![false, true]
);
assert_eq!(
detect("import * as ns from './state'; const x = ns; export const snap = x.foo;"),
vec![false, true, false]
);
}
#[test]
fn function_and_arrow_bodies_are_deferred() {
assert_eq!(
detect("import { counter } from './state'; export function read() { return counter; }"),
vec![false, false]
);
assert_eq!(
detect("import { counter } from './state'; export const read = () => counter;"),
vec![false, false]
);
assert_eq!(
detect("import { x } from './state'; export class C { m() { return x; } }"),
vec![false, false]
);
}
#[test]
fn immediately_invoked_functions_run_during_module_evaluation() {
assert_eq!(
detect("import { value } from './state'; export const snapshot = (() => value)();"),
vec![false, true]
);
assert_eq!(
detect(
"import { value } from './state'; export const snapshot = (function () { return value })();"
),
vec![false, true]
);
assert_eq!(
detect("import { value } from './state'; export const snapshot = ((x = value) => x)();"),
vec![false, true]
);
assert_eq!(
detect("import { value } from './state'; export const snapshot = (0, (() => value))();"),
vec![false, true]
);
assert_eq!(
detect(
"import { value } from './state'; export const snapshot = ((() => value), (() => 0))();"
),
vec![false, false]
);
}
#[test]
fn immediately_invoked_generator_only_runs_parameter_initializers() {
assert_eq!(
detect(
"import { value } from './state'; export const iterator = (function* (x = value) {})();"
),
vec![false, true]
);
assert_eq!(
detect(
"import { value } from './state'; export const iterator = (function* () { yield value })();"
),
vec![false, false]
);
}
#[test]
fn reexports_and_non_imports_are_not_reads() {
assert_eq!(detect("export { a } from './a';"), vec![false]);
assert_eq!(detect("import { a } from './a'; export { a };"), vec![false, false]);
assert_eq!(detect("const a = 1; export const b = a;"), vec![false, false]);
}
#[test]
fn default_export_value_is_a_read() {
assert_eq!(detect("import { a } from './a'; export default a;"), vec![false, true]);
}
#[test]
fn instance_field_initializers_are_conservatively_flagged() {
assert_eq!(detect("import { x } from './state'; export class C { f = x; }"), vec![false, true]);
}
#[test]
fn class_definition_time_reads_are_flagged() {
assert_eq!(
detect("import { x } from './state'; export class C { static s = x; }"),
vec![false, true]
);
assert_eq!(
detect("import { Base } from './state'; export class C extends Base {}"),
vec![false, true]
);
}
}