use std::path::Path;
use oxc::ast::ast::CommentContent;
use oxc::ast::ast::Program;
use oxc::ast_visit::VisitMut;
use oxc::diagnostics::Severity as OxcSeverity;
use oxc::minifier::{CompressOptions, Compressor, TreeShakeOptions};
use oxc::semantic::{Scoping, Stats};
use oxc::syntax::symbol::SymbolFlags;
use oxc::transformer::Transformer;
use oxc::transformer_plugins::{
InjectGlobalVariables, ReplaceGlobalDefines, ReplaceGlobalDefinesConfig,
};
use oxc_str::CompactStr;
use rolldown_common::{ConstExportMeta, ConstantValue, NormalizedBundlerOptions};
use rolldown_ecmascript::{EcmaAst, WithMutFields, semantic_builder_for_transform};
use rolldown_ecmascript_utils::contains_script_closing_tag;
use rolldown_error::{BatchedBuildDiagnostic, BuildDiagnostic, BuildResult, EventKind, Severity};
use rustc_hash::FxHashMap;
use crate::types::oxc_parse_type::OxcParseType;
use super::parse_to_ecma_ast::ParseToEcmaAstResult;
use super::tweak_ast_for_scanning::PreProcessor;
#[derive(Default)]
pub struct PreProcessEcmaAst {
stats: Stats,
}
impl PreProcessEcmaAst {
#[expect(clippy::too_many_arguments)]
pub fn build(
&mut self,
mut ast: EcmaAst,
stable_id: &str,
resolved_id: &str,
parsed_type: &OxcParseType,
replace_global_define_config: Option<&ReplaceGlobalDefinesConfig>,
bundle_options: &NormalizedBundlerOptions,
has_lazy_export: bool,
) -> BuildResult<ParseToEcmaAstResult> {
let source = ast.source().clone();
if !ast.program().directives.is_empty() && !ast.program().comments.is_empty() {
ast.program.with_mut(|WithMutFields { program, .. }| {
let mut i = 0;
for directive in &program.directives {
while i < program.comments.len() {
let comment = &mut program.comments[i];
if comment.attached_to == directive.span.start {
comment.attached_to = 0;
} else if comment.attached_to > directive.span.start {
break;
}
i += 1;
}
}
});
}
let semantic_ret = ast.program.with_dependent(|_owner, dep| {
semantic_builder_for_transform().with_check_syntax_error(true).build(&dep.program)
});
let (errors, warnings): (Vec<_>, Vec<_>) =
semantic_ret.errors.into_iter().partition(|w| w.severity == OxcSeverity::Error);
let mut warnings = if errors.is_empty() {
BuildDiagnostic::from_oxc_diagnostics(
warnings,
&source,
resolved_id,
Severity::Warning,
EventKind::ParseError,
)
} else {
return Err(BuildDiagnostic::from_oxc_diagnostics(
errors,
&source,
resolved_id,
Severity::Error,
EventKind::ParseError,
))?;
};
ast.program.with_dependent(|_owner, dep| {
for comment in
dep.program.comments.iter().filter(|c| c.content == CommentContent::PureNotApplied)
{
let span = comment.span;
let annotation = source[span.start as usize..span.end as usize].to_string();
warnings.push(BuildDiagnostic::invalid_annotation(
resolved_id.to_string(),
annotation,
source.clone(),
span,
));
}
});
self.stats = semantic_ret.semantic.stats();
let mut scoping = Some(semantic_ret.semantic.into_scoping());
let enum_member_value_map = {
let scoping_ref = scoping.as_mut().unwrap();
let mut enum_values: FxHashMap<CompactStr, FxHashMap<CompactStr, ConstExportMeta>> =
FxHashMap::default();
for symbol_id in scoping_ref.symbol_ids() {
let flags = scoping_ref.symbol_flags(symbol_id);
if !(flags.is_const_enum() || flags.contains(SymbolFlags::RegularEnum)) {
continue;
}
let Some(body_scopes) = scoping_ref.get_enum_body_scopes(symbol_id) else { continue };
let members =
enum_values.entry(CompactStr::from(scoping_ref.symbol_name(symbol_id))).or_default();
for &body_scope in body_scopes {
for (member_name, &member_sym) in scoping_ref.get_bindings(body_scope) {
if let Some(value) = scoping_ref.get_enum_member_value(member_sym) {
let rolldown_value = match value {
oxc::syntax::constant_value::ConstantValue::Number(n) => ConstantValue::Number(*n),
oxc::syntax::constant_value::ConstantValue::String(s) => {
ConstantValue::String(s.to_string())
}
};
members.insert(
CompactStr::from(member_name.as_str()),
ConstExportMeta::new(rolldown_value, false),
);
}
}
}
}
enum_values
};
if let Some(replace_global_define_config) = replace_global_define_config {
ast.program.with_mut(|WithMutFields { program, allocator, .. }| {
let ret = ReplaceGlobalDefines::new(allocator, replace_global_define_config.clone())
.build(scoping.take().unwrap(), program);
if !ret.changed {
scoping = Some(ret.scoping);
}
});
}
let is_not_js = !matches!(parsed_type, OxcParseType::Js);
let mut preserve_jsx = false;
if is_not_js
|| bundle_options.transform_options.should_transform_js()
|| contains_script_closing_tag(ast.source().as_bytes())
{
ast.program.with_mut(|WithMutFields { program, allocator, .. }| {
let transform_options = bundle_options
.transform_options
.options_for_file(is_not_js.then_some(Path::new(resolved_id)), &mut warnings)?;
if !transform_options.jsx.jsx_plugin {
preserve_jsx = true;
}
let scoping = self.recreate_scoping(&mut scoping, program);
let ret = Transformer::new(allocator, Path::new(stable_id), &transform_options)
.build_with_scoping(scoping, program);
let (errors, transformer_warnings): (Vec<_>, Vec<_>) =
ret.errors.into_iter().partition(|error| error.severity == OxcSeverity::Error);
if !errors.is_empty() {
return Err(BatchedBuildDiagnostic::from(BuildDiagnostic::from_oxc_diagnostics(
errors,
&source,
resolved_id,
Severity::Error,
EventKind::TransformError,
)));
}
warnings.extend(BuildDiagnostic::from_oxc_diagnostics(
transformer_warnings,
&source,
resolved_id,
Severity::Warning,
EventKind::ToleratedTransform,
));
Ok(())
})?;
}
if !bundle_options.inject.is_empty() {
ast.program.with_mut(|WithMutFields { program, allocator, .. }| {
let new_scoping = self.recreate_scoping(&mut scoping, program);
let inject_config = bundle_options.oxc_inject_global_variables_config.clone();
let ret = InjectGlobalVariables::new(allocator, inject_config).build(new_scoping, program);
if !ret.changed {
scoping = Some(ret.scoping);
}
});
}
if bundle_options.treeshake.is_some() && !has_lazy_export {
ast.program.with_mut(|WithMutFields { program, allocator, .. }| {
let scoping = self.recreate_scoping(&mut scoping, program);
let mut treeshake = TreeShakeOptions::from(&bundle_options.treeshake);
treeshake.invalid_import_side_effects = true;
let options = CompressOptions {
target: bundle_options.transform_options.target.clone(),
treeshake,
..CompressOptions::dce()
};
Compressor::new(allocator).dead_code_elimination_with_scoping(program, scoping, options);
});
}
let scoping = ast.program.with_mut(|WithMutFields { program, allocator, .. }| {
let mut pre_processor = PreProcessor::new(allocator, bundle_options.keep_names);
pre_processor.visit_program(program);
self.recreate_scoping(&mut None, program)
});
Ok(ParseToEcmaAstResult {
ast,
scoping,
has_lazy_export,
warnings,
preserve_jsx,
enum_member_value_map,
})
}
fn recreate_scoping(&mut self, scoping: &mut Option<Scoping>, program: &Program<'_>) -> Scoping {
if let Some(scoping) = scoping.take() {
return scoping;
}
let ret = semantic_builder_for_transform()
.with_stats(self.stats)
.build(program)
.semantic;
self.stats = ret.stats();
ret.into_scoping()
}
}