mirror of
https://github.com/swc-project/swc.git
synced 2024-10-04 04:07:18 +03:00
feat(es/analyzer): Extract the analyzer from the minifier to a separate crate (#6586)
This commit is contained in:
parent
da5e18e522
commit
e1d01d8b7a
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -3294,6 +3294,7 @@ dependencies = [
|
||||
"swc_ecma_transforms_react",
|
||||
"swc_ecma_transforms_testing",
|
||||
"swc_ecma_transforms_typescript",
|
||||
"swc_ecma_usage_analyzer",
|
||||
"swc_ecma_utils",
|
||||
"swc_ecma_visit",
|
||||
"swc_node_base",
|
||||
@ -3653,6 +3654,7 @@ dependencies = [
|
||||
"swc_ecma_testing",
|
||||
"swc_ecma_transforms_base",
|
||||
"swc_ecma_transforms_optimization",
|
||||
"swc_ecma_usage_analyzer",
|
||||
"swc_ecma_utils",
|
||||
"swc_ecma_visit",
|
||||
"swc_node_base",
|
||||
@ -4013,6 +4015,22 @@ dependencies = [
|
||||
"testing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_usage_analyzer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"indexmap",
|
||||
"rustc-hash",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
"swc_ecma_ast",
|
||||
"swc_ecma_utils",
|
||||
"swc_ecma_visit",
|
||||
"swc_timer",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_utils"
|
||||
version = "0.106.5"
|
||||
|
@ -146,6 +146,9 @@ ecma_minifier = ["__ecma", "swc_ecma_minifier"]
|
||||
# Enable swc_ecma_preset_env
|
||||
ecma_preset_env = ["__ecma", "swc_ecma_preset_env"]
|
||||
|
||||
# Enable swc_ecma_usage_analyzer
|
||||
ecma_usage_analyzer = ["__ecma", "swc_ecma_usage_analyzer"]
|
||||
|
||||
# Enable swc_css
|
||||
css_ast = ["__css", "swc_css_ast"]
|
||||
css_codegen = ["__css", "swc_css_codegen"]
|
||||
@ -366,6 +369,7 @@ swc_ecma_transforms_proposal = { optional = true, version = "0.145.5", path
|
||||
swc_ecma_transforms_react = { optional = true, version = "0.156.5", path = "../swc_ecma_transforms_react" }
|
||||
swc_ecma_transforms_testing = { optional = true, version = "0.115.6", path = "../swc_ecma_transforms_testing" }
|
||||
swc_ecma_transforms_typescript = { optional = true, version = "0.160.5", path = "../swc_ecma_transforms_typescript" }
|
||||
swc_ecma_usage_analyzer = { optional = true, version = "0.1.0", path = "../swc_ecma_usage_analyzer" }
|
||||
swc_ecma_utils = { optional = true, version = "0.106.5", path = "../swc_ecma_utils" }
|
||||
swc_ecma_visit = { optional = true, version = "0.81.3", path = "../swc_ecma_visit" }
|
||||
swc_node_base = { optional = true, version = "0.5.8", path = "../swc_node_base" }
|
||||
|
@ -109,6 +109,12 @@ pub mod ecma {
|
||||
pub use swc_ecma_preset_env::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ecma_usage_analyzer")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "ecma_usage_analyzer")))]
|
||||
pub mod usage_analyzer {
|
||||
pub use swc_ecma_usage_analyzer::*;
|
||||
}
|
||||
|
||||
// visit* interfaces
|
||||
#[cfg(feature = "__visit")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "__visit")))]
|
||||
|
@ -54,6 +54,7 @@ swc_ecma_codegen = { version = "0.128.5", path = "../swc_ecma_co
|
||||
swc_ecma_parser = { version = "0.123.5", path = "../swc_ecma_parser" }
|
||||
swc_ecma_transforms_base = { version = "0.112.5", path = "../swc_ecma_transforms_base" }
|
||||
swc_ecma_transforms_optimization = { version = "0.168.5", path = "../swc_ecma_transforms_optimization" }
|
||||
swc_ecma_usage_analyzer = { version = "0.1.0", path = "../swc_ecma_usage_analyzer" }
|
||||
swc_ecma_utils = { version = "0.106.5", path = "../swc_ecma_utils" }
|
||||
swc_ecma_visit = { version = "0.81.3", path = "../swc_ecma_visit" }
|
||||
swc_timer = { version = "0.17.20", path = "../swc_timer" }
|
||||
|
@ -2,12 +2,13 @@
|
||||
use rayon::prelude::*;
|
||||
use swc_common::{collections::AHashSet, pass::Repeated, util::take::Take, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_usage_analyzer::analyzer::UsageAnalyzer;
|
||||
use swc_ecma_utils::{find_pat_ids, StmtLike};
|
||||
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith};
|
||||
|
||||
use super::util::drop_invalid_stmts;
|
||||
use crate::{
|
||||
analyzer::{ProgramData, UsageAnalyzer},
|
||||
program_data::ProgramData,
|
||||
util::{is_hoisted_var_decl_without_init, sort::is_sorted_by, IsModuleItem, ModuleItemExt},
|
||||
};
|
||||
|
||||
@ -45,7 +46,7 @@ impl Hoister<'_> {
|
||||
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
|
||||
where
|
||||
T: StmtLike + IsModuleItem + ModuleItemExt,
|
||||
Vec<T>: for<'aa> VisitMutWith<Hoister<'aa>> + VisitWith<UsageAnalyzer>,
|
||||
Vec<T>: for<'aa> VisitMutWith<Hoister<'aa>> + VisitWith<UsageAnalyzer<ProgramData>>,
|
||||
{
|
||||
stmts.visit_mut_children_with(self);
|
||||
let len = stmts.len();
|
||||
|
@ -17,6 +17,7 @@ use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::simplify::{
|
||||
dead_branch_remover, expr_simplifier, ExprSimplifierConfig,
|
||||
};
|
||||
use swc_ecma_usage_analyzer::{analyzer::UsageAnalyzer, marks::Marks};
|
||||
use swc_ecma_visit::{as_folder, noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith};
|
||||
use swc_timer::timer;
|
||||
use tracing::{debug, error};
|
||||
@ -24,12 +25,11 @@ use tracing::{debug, error};
|
||||
pub(crate) use self::pure::{pure_optimizer, PureOptimizerConfig};
|
||||
use self::{hoist_decls::DeclHoisterConfig, optimize::optimizer};
|
||||
use crate::{
|
||||
analyzer::{analyze, ModuleInfo, UsageAnalyzer},
|
||||
compress::hoist_decls::decl_hoister,
|
||||
debug::{dump, AssertValid},
|
||||
marks::Marks,
|
||||
mode::Mode,
|
||||
option::CompressOptions,
|
||||
program_data::{analyze, ModuleInfo, ProgramData},
|
||||
util::{now, unit::CompileUnit},
|
||||
};
|
||||
|
||||
@ -100,7 +100,7 @@ where
|
||||
fn optimize_unit_repeatedly<N>(&mut self, n: &mut N)
|
||||
where
|
||||
N: CompileUnit
|
||||
+ VisitWith<UsageAnalyzer>
|
||||
+ VisitWith<UsageAnalyzer<ProgramData>>
|
||||
+ for<'aa> VisitMutWith<Compressor<'aa, M>>
|
||||
+ VisitWith<AssertValid>,
|
||||
{
|
||||
@ -147,7 +147,7 @@ where
|
||||
fn optimize_unit<N>(&mut self, n: &mut N)
|
||||
where
|
||||
N: CompileUnit
|
||||
+ VisitWith<UsageAnalyzer>
|
||||
+ VisitWith<UsageAnalyzer<ProgramData>>
|
||||
+ for<'aa> VisitMutWith<Compressor<'aa, M>>
|
||||
+ VisitWith<AssertValid>,
|
||||
{
|
||||
|
@ -2,15 +2,15 @@ use swc_atoms::js_word;
|
||||
use swc_common::{util::take::Take, EqIgnoreSpan, Spanned};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::simplify::expr_simplifier;
|
||||
use swc_ecma_usage_analyzer::alias::{collect_infects_from, AliasConfig};
|
||||
use swc_ecma_utils::{class_has_side_effect, find_pat_ids, ExprExt};
|
||||
use swc_ecma_visit::VisitMutWith;
|
||||
|
||||
use super::Optimizer;
|
||||
use crate::{
|
||||
alias::{collect_infects_from, AliasConfig},
|
||||
analyzer::VarUsageInfo,
|
||||
compress::optimize::util::is_valid_for_lhs,
|
||||
mode::Mode,
|
||||
program_data::VarUsageInfo,
|
||||
util::{
|
||||
idents_captured_by, idents_used_by, idents_used_by_ignoring_nested, size::SizeWithCtxt,
|
||||
},
|
||||
|
@ -8,6 +8,7 @@ use swc_common::{
|
||||
};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::debug_assert_valid;
|
||||
use swc_ecma_usage_analyzer::{analyzer::UsageAnalyzer, marks::Marks};
|
||||
use swc_ecma_utils::{
|
||||
prepend_stmts, undefined, ExprCtx, ExprExt, ExprFactory, IsEmpty, ModuleItemLike, StmtLike,
|
||||
Type, Value,
|
||||
@ -25,13 +26,12 @@ use super::util::{drop_invalid_stmts, is_fine_for_if_cons};
|
||||
#[cfg(feature = "debug")]
|
||||
use crate::debug::dump;
|
||||
use crate::{
|
||||
analyzer::{ModuleInfo, ProgramData, UsageAnalyzer},
|
||||
compress::util::is_pure_undefined,
|
||||
debug::AssertValid,
|
||||
marks::Marks,
|
||||
maybe_par,
|
||||
mode::Mode,
|
||||
option::CompressOptions,
|
||||
program_data::{ModuleInfo, ProgramData},
|
||||
util::{
|
||||
contains_eval, contains_leaping_continue_with_label, make_number, ExprOptExt, ModuleItemExt,
|
||||
},
|
||||
@ -332,7 +332,7 @@ where
|
||||
fn handle_stmt_likes<T>(&mut self, stmts: &mut Vec<T>)
|
||||
where
|
||||
T: StmtLike + ModuleItemLike + ModuleItemExt + VisitMutWith<Self> + VisitWith<AssertValid>,
|
||||
Vec<T>: VisitMutWith<Self> + VisitWith<UsageAnalyzer> + VisitWith<AssertValid>,
|
||||
Vec<T>: VisitMutWith<Self> + VisitWith<UsageAnalyzer<ProgramData>> + VisitWith<AssertValid>,
|
||||
{
|
||||
let mut use_asm = false;
|
||||
let prepend_stmts = self.prepend_stmts.take();
|
||||
|
@ -3,6 +3,10 @@ use std::mem::take;
|
||||
use swc_atoms::js_word;
|
||||
use swc_common::{util::take::Take, Spanned, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_usage_analyzer::{
|
||||
alias::{collect_infects_from, AccessKind, AliasConfig},
|
||||
util::is_global_var_with_pure_property_access,
|
||||
};
|
||||
use swc_ecma_utils::{
|
||||
contains_arguments, contains_this_expr, prepend_stmts, undefined, ExprExt, IdentUsageFinder,
|
||||
StmtLike,
|
||||
@ -15,17 +19,13 @@ use super::{is_pure_undefined, Optimizer};
|
||||
#[cfg(feature = "debug")]
|
||||
use crate::debug::dump;
|
||||
use crate::{
|
||||
alias::{collect_infects_from, AccessKind, AliasConfig},
|
||||
compress::{
|
||||
optimize::{unused::PropertyAccessOpts, util::replace_id_with_expr},
|
||||
util::{is_directive, is_ident_used_by, replace_expr},
|
||||
},
|
||||
mode::Mode,
|
||||
option::CompressOptions,
|
||||
util::{
|
||||
idents_used_by, idents_used_by_ignoring_nested, is_global_var_with_pure_property_access,
|
||||
ExprOptExt, ModuleItemExt,
|
||||
},
|
||||
util::{idents_used_by, idents_used_by_ignoring_nested, ExprOptExt, ModuleItemExt},
|
||||
};
|
||||
|
||||
/// Methods related to the option `sequences`. All methods are noop if
|
||||
@ -1170,8 +1170,8 @@ where
|
||||
.expand_infected(self.module_info, ids_used_by_a_init, 64);
|
||||
|
||||
let deps = match deps {
|
||||
Ok(v) => v,
|
||||
Err(()) => return false,
|
||||
Some(v) => v,
|
||||
_ => return false,
|
||||
};
|
||||
if deps.contains(&(e.to_id(), AccessKind::Reference))
|
||||
|| deps.contains(&(e.to_id(), AccessKind::Call))
|
||||
|
@ -1,6 +1,7 @@
|
||||
use swc_atoms::js_word;
|
||||
use swc_common::{util::take::Take, Span, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_usage_analyzer::util::is_global_var_with_pure_property_access;
|
||||
use swc_ecma_utils::contains_ident_ref;
|
||||
|
||||
use super::Optimizer;
|
||||
@ -8,7 +9,6 @@ use super::Optimizer;
|
||||
use crate::debug::dump;
|
||||
use crate::{
|
||||
compress::optimize::util::extract_class_side_effect, mode::Mode, option::PureGetterOption,
|
||||
util::is_global_var_with_pure_property_access,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
|
@ -4,20 +4,17 @@ use swc_atoms::js_word;
|
||||
use swc_common::{iter::IdentifyLast, util::take::Take, Span, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::debug_assert_valid;
|
||||
use swc_ecma_usage_analyzer::util::is_global_var_with_pure_property_access;
|
||||
use swc_ecma_utils::{
|
||||
ExprExt, ExprFactory, IdentUsageFinder, Type,
|
||||
Value::{self, Known},
|
||||
};
|
||||
|
||||
use super::Pure;
|
||||
use crate::{
|
||||
compress::{
|
||||
pure::strings::{convert_str_value_to_tpl_cooked, convert_str_value_to_tpl_raw},
|
||||
util::is_pure_undefined,
|
||||
},
|
||||
util::is_global_var_with_pure_property_access,
|
||||
use crate::compress::{
|
||||
pure::strings::{convert_str_value_to_tpl_cooked, convert_str_value_to_tpl_raw},
|
||||
util::is_pure_undefined,
|
||||
};
|
||||
|
||||
impl Pure<'_> {
|
||||
pub(super) fn remove_invalid(&mut self, e: &mut Expr) {
|
||||
match e {
|
||||
|
@ -5,6 +5,7 @@ use rayon::prelude::*;
|
||||
use swc_common::{pass::Repeated, util::take::Take, SyntaxContext, DUMMY_SP, GLOBALS};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::debug_assert_valid;
|
||||
use swc_ecma_usage_analyzer::marks::Marks;
|
||||
use swc_ecma_utils::{undefined, ExprCtx};
|
||||
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith};
|
||||
#[cfg(feature = "debug")]
|
||||
@ -14,7 +15,7 @@ use self::{ctx::Ctx, misc::DropOpts};
|
||||
#[cfg(feature = "debug")]
|
||||
use crate::debug::dump;
|
||||
use crate::{
|
||||
analyzer::ProgramData, debug::AssertValid, marks::Marks, maybe_par, option::CompressOptions,
|
||||
debug::AssertValid, maybe_par, option::CompressOptions, program_data::ProgramData,
|
||||
util::ModuleItemExt,
|
||||
};
|
||||
|
||||
|
@ -5,12 +5,12 @@ use swc_atoms::js_word;
|
||||
use swc_common::{collections::AHashMap, SyntaxContext, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::simplify::{expr_simplifier, ExprSimplifierConfig};
|
||||
use swc_ecma_usage_analyzer::marks::Marks;
|
||||
use swc_ecma_utils::{undefined, ExprCtx, ExprExt};
|
||||
use swc_ecma_visit::VisitMutWith;
|
||||
|
||||
use crate::{
|
||||
compress::{compressor, pure_optimizer, PureOptimizerConfig},
|
||||
marks::Marks,
|
||||
mode::Mode,
|
||||
};
|
||||
|
||||
|
@ -41,14 +41,13 @@ use once_cell::sync::Lazy;
|
||||
use swc_common::{comments::Comments, pass::Repeated, sync::Lrc, SourceMap, SyntaxContext};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_transforms_optimization::debug_assert_valid;
|
||||
use swc_ecma_usage_analyzer::marks::Marks;
|
||||
use swc_ecma_visit::VisitMutWith;
|
||||
use swc_timer::timer;
|
||||
|
||||
pub use crate::pass::unique_scope::unique_scope;
|
||||
use crate::{
|
||||
analyzer::ModuleInfo,
|
||||
compress::{compressor, pure_optimizer, PureOptimizerConfig},
|
||||
marks::Marks,
|
||||
metadata::info_marker,
|
||||
mode::{Minification, Mode},
|
||||
option::{CompressOptions, ExtraOptions, MinifyOptions},
|
||||
@ -61,25 +60,28 @@ use crate::{
|
||||
postcompress::postcompress_optimizer,
|
||||
precompress::precompress_optimizer,
|
||||
},
|
||||
program_data::ModuleInfo,
|
||||
timing::Timings,
|
||||
util::base54::CharFreq,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod alias;
|
||||
mod analyzer;
|
||||
mod compress;
|
||||
mod debug;
|
||||
pub mod eval;
|
||||
pub mod marks;
|
||||
mod metadata;
|
||||
mod mode;
|
||||
pub mod option;
|
||||
mod pass;
|
||||
mod program_data;
|
||||
pub mod timing;
|
||||
mod util;
|
||||
|
||||
pub mod marks {
|
||||
pub use swc_ecma_usage_analyzer::marks::Marks;
|
||||
}
|
||||
|
||||
const DISABLE_BUGGY_PASSES: bool = true;
|
||||
|
||||
pub(crate) static CPU_COUNT: Lazy<usize> = Lazy::new(num_cpus::get);
|
||||
|
@ -3,12 +3,13 @@ use swc_common::{
|
||||
EqIgnoreSpan, Span, SyntaxContext,
|
||||
};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_usage_analyzer::marks::Marks;
|
||||
use swc_ecma_utils::find_pat_ids;
|
||||
use swc_ecma_visit::{
|
||||
noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
|
||||
};
|
||||
|
||||
use crate::{marks::Marks, option::CompressOptions};
|
||||
use crate::option::CompressOptions;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -10,8 +10,8 @@ use swc_ecma_ast::{
|
||||
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
|
||||
|
||||
use crate::{
|
||||
analyzer::{analyze, ModuleInfo, ProgramData},
|
||||
option::ManglePropertiesOptions,
|
||||
program_data::{analyze, ModuleInfo, ProgramData},
|
||||
util::base54::Base54Chars,
|
||||
};
|
||||
|
||||
|
@ -1,14 +1,207 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use swc_common::collections::AHashSet;
|
||||
use swc_ecma_ast::*;
|
||||
|
||||
use super::{ScopeDataLike, Storage, VarDataLike};
|
||||
use crate::{
|
||||
alias::Access,
|
||||
analyzer::{ctx::Ctx, ProgramData, ScopeData, ScopeKind, VarUsageInfo},
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashSet},
|
||||
hash::BuildHasherDefault,
|
||||
};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
|
||||
use swc_atoms::JsWord;
|
||||
use swc_common::{
|
||||
collections::{AHashMap, AHashSet},
|
||||
SyntaxContext,
|
||||
};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_usage_analyzer::{
|
||||
alias::{Access, AccessKind},
|
||||
analyzer::{
|
||||
analyze_with_storage,
|
||||
storage::{ScopeDataLike, Storage, VarDataLike},
|
||||
Ctx, ScopeKind, UsageAnalyzer,
|
||||
},
|
||||
marks::Marks,
|
||||
};
|
||||
use swc_ecma_visit::VisitWith;
|
||||
|
||||
pub(crate) fn analyze<N>(n: &N, _module_info: &ModuleInfo, marks: Option<Marks>) -> ProgramData
|
||||
where
|
||||
N: VisitWith<UsageAnalyzer<ProgramData>>,
|
||||
{
|
||||
analyze_with_storage::<ProgramData, _>(n, marks)
|
||||
}
|
||||
|
||||
/// Analyzed info of a whole program we are working on.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ProgramData {
|
||||
pub(crate) vars: FxHashMap<Id, VarUsageInfo>,
|
||||
|
||||
pub(crate) top: ScopeData,
|
||||
|
||||
pub(crate) scopes: FxHashMap<SyntaxContext, ScopeData>,
|
||||
|
||||
initialized_vars: IndexSet<Id, ahash::RandomState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct ScopeData {
|
||||
pub(crate) has_with_stmt: bool,
|
||||
pub(crate) has_eval_call: bool,
|
||||
pub(crate) used_arguments: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ModuleInfo {
|
||||
/// Imported identifiers which should be treated as a black box.
|
||||
///
|
||||
/// Imports from `@swc/helpers` are excluded as helpers are not modified by
|
||||
/// accessing/calling other modules.
|
||||
pub(crate) blackbox_imports: AHashSet<Id>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct VarUsageInfo {
|
||||
pub(crate) inline_prevented: bool,
|
||||
|
||||
/// The number of direct reference to this identifier.
|
||||
pub(crate) ref_count: u32,
|
||||
|
||||
/// `true` if a variable is conditionally initialized.
|
||||
pub(crate) cond_init: bool,
|
||||
|
||||
/// `false` if it's only used.
|
||||
pub(crate) declared: bool,
|
||||
pub(crate) declared_count: u32,
|
||||
|
||||
/// `true` if the enclosing function defines this variable as a parameter.
|
||||
pub(crate) declared_as_fn_param: bool,
|
||||
|
||||
pub(crate) declared_as_fn_decl: bool,
|
||||
pub(crate) declared_as_fn_expr: bool,
|
||||
|
||||
pub(crate) assign_count: u32,
|
||||
pub(crate) mutation_by_call_count: u32,
|
||||
|
||||
/// The number of direct and indirect reference to this identifier.
|
||||
/// ## Things to note
|
||||
///
|
||||
/// - Update is counted as usage, but assign is not
|
||||
pub(crate) usage_count: u32,
|
||||
|
||||
/// The variable itself is modified.
|
||||
reassigned_with_assignment: bool,
|
||||
reassigned_with_var_decl: bool,
|
||||
/// The variable itself or a property of it is modified.
|
||||
pub(crate) mutated: bool,
|
||||
|
||||
pub(crate) has_property_access: bool,
|
||||
pub(crate) has_property_mutation: bool,
|
||||
|
||||
pub(crate) exported: bool,
|
||||
/// True if used **above** the declaration or in init. (Not eval order).
|
||||
pub(crate) used_above_decl: bool,
|
||||
/// `true` if it's declared by function parameters or variables declared in
|
||||
/// a closest function and used only within it and not used by child
|
||||
/// functions.
|
||||
pub(crate) is_fn_local: bool,
|
||||
|
||||
pub(crate) executed_multiple_time: bool,
|
||||
pub(crate) used_in_cond: bool,
|
||||
|
||||
pub(crate) var_kind: Option<VarDeclKind>,
|
||||
pub(crate) var_initialized: bool,
|
||||
|
||||
pub(crate) declared_as_catch_param: bool,
|
||||
|
||||
pub(crate) no_side_effect_for_member_access: bool,
|
||||
|
||||
pub(crate) callee_count: u32,
|
||||
|
||||
pub(crate) used_as_arg: bool,
|
||||
|
||||
pub(crate) indexed_with_dynamic_key: bool,
|
||||
|
||||
pub(crate) pure_fn: bool,
|
||||
|
||||
/// `infects_to`. This should be renamed, but it will be done with another
|
||||
/// PR. (because it's hard to review)
|
||||
infects: Vec<Access>,
|
||||
|
||||
pub(crate) used_in_non_child_fn: bool,
|
||||
/// Only **string** properties.
|
||||
pub(crate) accessed_props: Box<AHashMap<JsWord, u32>>,
|
||||
|
||||
pub(crate) used_recursively: bool,
|
||||
}
|
||||
|
||||
impl Default for VarUsageInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inline_prevented: Default::default(),
|
||||
ref_count: Default::default(),
|
||||
cond_init: Default::default(),
|
||||
declared: Default::default(),
|
||||
declared_count: Default::default(),
|
||||
declared_as_fn_param: Default::default(),
|
||||
declared_as_fn_decl: Default::default(),
|
||||
declared_as_fn_expr: Default::default(),
|
||||
assign_count: Default::default(),
|
||||
mutation_by_call_count: Default::default(),
|
||||
usage_count: Default::default(),
|
||||
reassigned_with_assignment: Default::default(),
|
||||
reassigned_with_var_decl: Default::default(),
|
||||
mutated: Default::default(),
|
||||
has_property_access: Default::default(),
|
||||
has_property_mutation: Default::default(),
|
||||
exported: Default::default(),
|
||||
used_above_decl: Default::default(),
|
||||
is_fn_local: true,
|
||||
executed_multiple_time: Default::default(),
|
||||
used_in_cond: Default::default(),
|
||||
var_kind: Default::default(),
|
||||
var_initialized: Default::default(),
|
||||
declared_as_catch_param: Default::default(),
|
||||
no_side_effect_for_member_access: Default::default(),
|
||||
callee_count: Default::default(),
|
||||
used_as_arg: Default::default(),
|
||||
indexed_with_dynamic_key: Default::default(),
|
||||
pure_fn: Default::default(),
|
||||
infects: Default::default(),
|
||||
used_in_non_child_fn: Default::default(),
|
||||
accessed_props: Default::default(),
|
||||
used_recursively: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VarUsageInfo {
|
||||
pub(crate) fn is_mutated_only_by_one_call(&self) -> bool {
|
||||
self.assign_count == 0 && self.mutation_by_call_count == 1
|
||||
}
|
||||
|
||||
pub(crate) fn is_infected(&self) -> bool {
|
||||
!self.infects.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn reassigned(&self) -> bool {
|
||||
self.reassigned_with_assignment
|
||||
|| self.reassigned_with_var_decl
|
||||
|| (u32::from(self.var_initialized)
|
||||
+ u32::from(self.declared_as_catch_param)
|
||||
+ u32::from(self.declared_as_fn_param)
|
||||
+ self.assign_count)
|
||||
> 1
|
||||
}
|
||||
|
||||
pub(crate) fn can_inline_var(&self) -> bool {
|
||||
!self.mutated
|
||||
|| (self.assign_count == 0 && !self.reassigned() && !self.has_property_mutation)
|
||||
}
|
||||
|
||||
pub(crate) fn can_inline_fn_once(&self) -> bool {
|
||||
self.callee_count > 0
|
||||
|| !self.executed_multiple_time && (self.is_fn_local || !self.used_in_non_child_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for ProgramData {
|
||||
type ScopeData = ScopeData;
|
||||
type VarData = VarUsageInfo;
|
||||
@ -156,7 +349,10 @@ impl Storage for ProgramData {
|
||||
let v = self.vars.entry(i.to_id()).or_default();
|
||||
|
||||
if has_init && (v.declared || v.var_initialized) {
|
||||
trace_op!("declare_decl(`{}`): Already declared", i);
|
||||
#[cfg(feature = "debug")]
|
||||
{
|
||||
tracing::trace!("declare_decl(`{}`): Already declared", i);
|
||||
}
|
||||
|
||||
v.mutated = true;
|
||||
v.reassigned_with_var_decl = true;
|
||||
@ -217,6 +413,185 @@ impl ScopeDataLike for ScopeData {
|
||||
}
|
||||
}
|
||||
|
||||
impl VarDataLike for VarUsageInfo {
|
||||
fn mark_declared_as_fn_param(&mut self) {
|
||||
self.declared_as_fn_param = true;
|
||||
}
|
||||
|
||||
fn mark_declared_as_fn_decl(&mut self) {
|
||||
self.declared_as_fn_decl = true;
|
||||
}
|
||||
|
||||
fn mark_declared_as_fn_expr(&mut self) {
|
||||
self.declared_as_fn_expr = true;
|
||||
}
|
||||
|
||||
fn mark_has_property_access(&mut self) {
|
||||
self.has_property_access = true;
|
||||
}
|
||||
|
||||
fn mark_has_property_mutation(&mut self) {
|
||||
self.has_property_mutation = true;
|
||||
}
|
||||
|
||||
fn mark_used_as_callee(&mut self) {
|
||||
self.callee_count += 1;
|
||||
}
|
||||
|
||||
fn mark_used_as_arg(&mut self) {
|
||||
self.used_as_arg = true
|
||||
}
|
||||
|
||||
fn mark_indexed_with_dynamic_key(&mut self) {
|
||||
self.indexed_with_dynamic_key = true;
|
||||
}
|
||||
|
||||
fn add_accessed_property(&mut self, name: swc_atoms::JsWord) {
|
||||
*self.accessed_props.entry(name).or_default() += 1;
|
||||
}
|
||||
|
||||
fn mark_mutated(&mut self) {
|
||||
self.mutated = true;
|
||||
}
|
||||
|
||||
fn mark_reassigned_with_assign(&mut self) {
|
||||
self.reassigned_with_assignment = true;
|
||||
}
|
||||
|
||||
fn add_infects_to(&mut self, other: Access) {
|
||||
self.infects.push(other);
|
||||
}
|
||||
|
||||
fn prevent_inline(&mut self) {
|
||||
self.inline_prevented = true;
|
||||
}
|
||||
|
||||
fn mark_initialized_with_safe_value(&mut self) {
|
||||
self.no_side_effect_for_member_access = true;
|
||||
}
|
||||
|
||||
fn mark_as_pure_fn(&mut self) {
|
||||
self.pure_fn = true;
|
||||
}
|
||||
|
||||
fn mark_used_above_decl(&mut self) {
|
||||
self.used_above_decl = true;
|
||||
}
|
||||
|
||||
fn mark_used_recursively(&mut self) {
|
||||
self.used_recursively = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgramData {
|
||||
pub(crate) fn expand_infected(
|
||||
&self,
|
||||
module_info: &ModuleInfo,
|
||||
ids: FxHashSet<Access>,
|
||||
max_num: usize,
|
||||
) -> Option<FxHashSet<Access>> {
|
||||
let init =
|
||||
HashSet::with_capacity_and_hasher(max_num, BuildHasherDefault::<FxHasher>::default());
|
||||
ids.into_iter()
|
||||
.try_fold(init, |mut res, id| {
|
||||
let mut ids = Vec::with_capacity(max_num);
|
||||
ids.push(id);
|
||||
let mut ranges = vec![0..1usize];
|
||||
loop {
|
||||
let range = ranges.remove(0);
|
||||
for index in range {
|
||||
let iid = ids.get(index).unwrap();
|
||||
|
||||
// Abort on imported variables, because we can't analyze them
|
||||
if module_info.blackbox_imports.contains(&iid.0) {
|
||||
return Err(());
|
||||
}
|
||||
if !res.insert(iid.clone()) {
|
||||
continue;
|
||||
}
|
||||
if res.len() >= max_num {
|
||||
return Err(());
|
||||
}
|
||||
if let Some(info) = self.vars.get(&iid.0) {
|
||||
let infects = &info.infects;
|
||||
if !infects.is_empty() {
|
||||
let old_len = ids.len();
|
||||
|
||||
// This is not a call, so effects from call can be skipped
|
||||
let can_skip_non_call = matches!(iid.1, AccessKind::Reference)
|
||||
|| (info.declared_count == 1
|
||||
&& info.declared_as_fn_decl
|
||||
&& !info.reassigned());
|
||||
|
||||
if can_skip_non_call {
|
||||
ids.extend(
|
||||
infects
|
||||
.iter()
|
||||
.filter(|(_, kind)| *kind != AccessKind::Call)
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
ids.extend_from_slice(infects.as_slice());
|
||||
}
|
||||
let new_len = ids.len();
|
||||
ranges.push(old_len..new_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ranges.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn contains_unresolved(&self, e: &Expr) -> bool {
|
||||
match e {
|
||||
Expr::Ident(i) => {
|
||||
if let Some(v) = self.vars.get(&i.to_id()) {
|
||||
return !v.declared;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Expr::Member(MemberExpr { obj, prop, .. }) => {
|
||||
if self.contains_unresolved(obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let MemberProp::Computed(prop) = prop {
|
||||
if self.contains_unresolved(&prop.expr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
Expr::Call(CallExpr {
|
||||
callee: Callee::Expr(callee),
|
||||
args,
|
||||
..
|
||||
}) => {
|
||||
if self.contains_unresolved(callee) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if args.iter().any(|arg| self.contains_unresolved(&arg.expr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgramData {
|
||||
fn report(&mut self, i: Id, ctx: Ctx, is_modify: bool, dejavu: &mut AHashSet<Id>) {
|
||||
// trace!("report({}{:?})", i.0, i.1);
|
||||
@ -293,73 +668,3 @@ impl ProgramData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VarDataLike for VarUsageInfo {
|
||||
fn mark_declared_as_fn_param(&mut self) {
|
||||
self.declared_as_fn_param = true;
|
||||
}
|
||||
|
||||
fn mark_declared_as_fn_decl(&mut self) {
|
||||
self.declared_as_fn_decl = true;
|
||||
}
|
||||
|
||||
fn mark_declared_as_fn_expr(&mut self) {
|
||||
self.declared_as_fn_expr = true;
|
||||
}
|
||||
|
||||
fn mark_has_property_access(&mut self) {
|
||||
self.has_property_access = true;
|
||||
}
|
||||
|
||||
fn mark_has_property_mutation(&mut self) {
|
||||
self.has_property_mutation = true;
|
||||
}
|
||||
|
||||
fn mark_used_as_callee(&mut self) {
|
||||
self.callee_count += 1;
|
||||
}
|
||||
|
||||
fn mark_used_as_arg(&mut self) {
|
||||
self.used_as_arg = true
|
||||
}
|
||||
|
||||
fn mark_indexed_with_dynamic_key(&mut self) {
|
||||
self.indexed_with_dynamic_key = true;
|
||||
}
|
||||
|
||||
fn add_accessed_property(&mut self, name: swc_atoms::JsWord) {
|
||||
*self.accessed_props.entry(name).or_default() += 1;
|
||||
}
|
||||
|
||||
fn mark_mutated(&mut self) {
|
||||
self.mutated = true;
|
||||
}
|
||||
|
||||
fn mark_reassigned_with_assign(&mut self) {
|
||||
self.reassigned_with_assignment = true;
|
||||
}
|
||||
|
||||
fn add_infects_to(&mut self, other: Access) {
|
||||
self.infects.push(other);
|
||||
}
|
||||
|
||||
fn prevent_inline(&mut self) {
|
||||
self.inline_prevented = true;
|
||||
}
|
||||
|
||||
fn mark_initialized_with_safe_value(&mut self) {
|
||||
self.no_side_effect_for_member_access = true;
|
||||
}
|
||||
|
||||
fn mark_as_pure_fn(&mut self) {
|
||||
self.pure_fn = true;
|
||||
}
|
||||
|
||||
fn mark_used_above_decl(&mut self) {
|
||||
self.used_above_decl = true;
|
||||
}
|
||||
|
||||
fn mark_used_recursively(&mut self) {
|
||||
self.used_recursively = true;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use swc_atoms::{js_word, JsWord};
|
||||
use swc_atoms::js_word;
|
||||
use swc_common::{util::take::Take, Mark, Span, Spanned, DUMMY_SP};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_utils::{ModuleItemLike, StmtLike, Value};
|
||||
@ -12,57 +12,6 @@ pub(crate) mod size;
|
||||
pub(crate) mod sort;
|
||||
pub(crate) mod unit;
|
||||
|
||||
pub(crate) fn is_global_var_with_pure_property_access(s: &JsWord) -> bool {
|
||||
match *s {
|
||||
js_word!("JSON")
|
||||
| js_word!("Array")
|
||||
| js_word!("String")
|
||||
| js_word!("Object")
|
||||
| js_word!("Number")
|
||||
| js_word!("Date")
|
||||
| js_word!("BigInt")
|
||||
| js_word!("Boolean")
|
||||
| js_word!("Math")
|
||||
| js_word!("Error") => return true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
matches!(
|
||||
&**s,
|
||||
"console"
|
||||
| "clearInterval"
|
||||
| "clearTimeout"
|
||||
| "setInterval"
|
||||
| "setTimeout"
|
||||
| "btoa"
|
||||
| "decodeURI"
|
||||
| "decodeURIComponent"
|
||||
| "encodeURI"
|
||||
| "encodeURIComponent"
|
||||
| "escape"
|
||||
| "eval"
|
||||
| "EvalError"
|
||||
| "Function"
|
||||
| "isFinite"
|
||||
| "isNaN"
|
||||
| "JSON"
|
||||
| "parseFloat"
|
||||
| "parseInt"
|
||||
| "RegExp"
|
||||
| "RangeError"
|
||||
| "ReferenceError"
|
||||
| "SyntaxError"
|
||||
| "TypeError"
|
||||
| "unescape"
|
||||
| "URIError"
|
||||
| "atob"
|
||||
| "globalThis"
|
||||
| "NaN"
|
||||
| "Symbol"
|
||||
| "Promise"
|
||||
)
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) fn make_number(span: Span, value: f64) -> Expr {
|
||||
trace_op!("Creating a numeric literal");
|
||||
@ -504,43 +453,6 @@ where
|
||||
v.ids
|
||||
}
|
||||
|
||||
pub(crate) fn can_end_conditionally(s: &Stmt) -> bool {
|
||||
///
|
||||
///`ignore_always`: If true, [Stmt::Return] will be ignored.
|
||||
fn can_end(s: &Stmt, ignore_always: bool) -> bool {
|
||||
match s {
|
||||
Stmt::If(s) => {
|
||||
can_end(&s.cons, false)
|
||||
|| s.alt
|
||||
.as_deref()
|
||||
.map(|s| can_end(s, false))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
Stmt::Switch(s) => s
|
||||
.cases
|
||||
.iter()
|
||||
.any(|case| case.cons.iter().any(|s| can_end(s, false))),
|
||||
|
||||
Stmt::DoWhile(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::While(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::For(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::ForOf(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::ForIn(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::Return(..) | Stmt::Break(..) | Stmt::Continue(..) => !ignore_always,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
can_end(s, true)
|
||||
}
|
||||
|
||||
pub fn now() -> Option<Instant> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
|
34
crates/swc_ecma_usage_analyzer/Cargo.toml
Normal file
34
crates/swc_ecma_usage_analyzer/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
|
||||
description = "EcmaScript variable usage analyzer"
|
||||
documentation = "https://rustdoc.swc.rs/swc_ecma_usage_analyzer/"
|
||||
edition = "2021"
|
||||
include = ["Cargo.toml", "src/**/*.rs"]
|
||||
license = "Apache-2.0"
|
||||
name = "swc_ecma_usage_analyzer"
|
||||
repository = "https://github.com/swc-project/swc.git"
|
||||
version = "0.1.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[features]
|
||||
# This enables global concurrent mode
|
||||
concurrent = ["swc_common/concurrent", "indexmap/rayon"]
|
||||
trace-ast = []
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.7.6"
|
||||
indexmap = "1.6.1"
|
||||
rustc-hash = "1.1.0"
|
||||
swc_atoms = { version = "0.4.25", path = "../swc_atoms" }
|
||||
swc_common = { version = "0.29.19", path = "../swc_common" }
|
||||
swc_ecma_ast = { version = "0.95.3", path = "../swc_ecma_ast" }
|
||||
swc_ecma_utils = { version = "0.106.5", path = "../swc_ecma_utils" }
|
||||
swc_ecma_visit = { version = "0.81.3", path = "../swc_ecma_visit" }
|
||||
swc_timer = { version = "0.17.20", path = "../swc_timer" }
|
||||
tracing = "0.1.32"
|
@ -15,7 +15,7 @@ impl<'a> InfectionCollector<'a> {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(super) struct Ctx {
|
||||
pub struct Ctx {
|
||||
pub track_expr_ident: bool,
|
||||
pub is_callee: bool,
|
||||
}
|
@ -6,13 +6,13 @@ use swc_ecma_ast::*;
|
||||
use swc_ecma_utils::{collect_decls, BindingCollector};
|
||||
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
||||
|
||||
use self::ctx::Ctx;
|
||||
pub use self::ctx::Ctx;
|
||||
use crate::{marks::Marks, util::is_global_var_with_pure_property_access};
|
||||
|
||||
mod ctx;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AliasConfig {
|
||||
pub struct AliasConfig {
|
||||
pub marks: Option<Marks>,
|
||||
pub ignore_nested: bool,
|
||||
/// TODO(kdy1): This field is used for sequential inliner.
|
||||
@ -20,7 +20,7 @@ pub(crate) struct AliasConfig {
|
||||
pub need_all: bool,
|
||||
}
|
||||
|
||||
pub(crate) trait InfectableNode {
|
||||
pub trait InfectableNode {
|
||||
fn is_fn_or_arrow_expr(&self) -> bool;
|
||||
}
|
||||
|
||||
@ -32,10 +32,7 @@ impl InfectableNode for Function {
|
||||
|
||||
impl InfectableNode for Expr {
|
||||
fn is_fn_or_arrow_expr(&self) -> bool {
|
||||
match self {
|
||||
Expr::Arrow(..) | Expr::Fn(..) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, Expr::Arrow(..) | Expr::Fn(..))
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,14 +47,14 @@ where
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
||||
pub(crate) enum AccessKind {
|
||||
pub enum AccessKind {
|
||||
Reference,
|
||||
Call,
|
||||
}
|
||||
|
||||
pub(crate) type Access = (Id, AccessKind);
|
||||
pub type Access = (Id, AccessKind);
|
||||
|
||||
pub(crate) fn collect_infects_from<N>(node: &N, config: AliasConfig) -> FxHashSet<Access>
|
||||
pub fn collect_infects_from<N>(node: &N, config: AliasConfig) -> FxHashSet<Access>
|
||||
where
|
||||
N: InfectableNode
|
||||
+ VisitWith<BindingCollector<Id>>
|
||||
@ -89,7 +86,7 @@ where
|
||||
visitor.accesses
|
||||
}
|
||||
|
||||
pub(crate) struct InfectionCollector<'a> {
|
||||
pub struct InfectionCollector<'a> {
|
||||
#[allow(unused)]
|
||||
config: AliasConfig,
|
||||
unresolved_ctxt: Option<SyntaxContext>,
|
||||
@ -107,10 +104,8 @@ impl InfectionCollector<'_> {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.unresolved_ctxt == Some(e.1) {
|
||||
if is_global_var_with_pure_property_access(&e.0) {
|
||||
return;
|
||||
}
|
||||
if self.unresolved_ctxt == Some(e.1) && is_global_var_with_pure_property_access(&e.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.accesses.insert((
|
@ -21,45 +21,45 @@ where
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct Ctx {
|
||||
pub struct Ctx {
|
||||
/// See [crate::marks::Marks]
|
||||
pub(super) skip_standalone: bool,
|
||||
pub skip_standalone: bool,
|
||||
|
||||
pub(super) var_decl_kind_of_pat: Option<VarDeclKind>,
|
||||
pub var_decl_kind_of_pat: Option<VarDeclKind>,
|
||||
|
||||
pub(super) in_decl_with_no_side_effect_for_member_access: bool,
|
||||
pub in_decl_with_no_side_effect_for_member_access: bool,
|
||||
|
||||
pub(super) in_pat_of_var_decl: bool,
|
||||
pub(super) in_pat_of_var_decl_with_init: bool,
|
||||
pub(super) in_pat_of_param: bool,
|
||||
pub(super) in_catch_param: bool,
|
||||
pub in_pat_of_var_decl: bool,
|
||||
pub in_pat_of_var_decl_with_init: bool,
|
||||
pub in_pat_of_param: bool,
|
||||
pub in_catch_param: bool,
|
||||
/// `true` for `foo.bar` and `false` for `foo` in `foo.bar++`
|
||||
pub(super) is_exact_reassignment: bool,
|
||||
pub is_exact_reassignment: bool,
|
||||
|
||||
pub(super) is_callee: bool,
|
||||
pub is_callee: bool,
|
||||
|
||||
/// `true` for arguments of [swc_ecma_ast::Expr::Call] or
|
||||
/// [swc_ecma_ast::Expr::New]
|
||||
pub(super) in_call_arg: bool,
|
||||
pub in_call_arg: bool,
|
||||
|
||||
/// `false` for `array` in `array.length.
|
||||
pub(super) is_exact_arg: bool,
|
||||
pub is_exact_arg: bool,
|
||||
|
||||
pub(super) in_await_arg: bool,
|
||||
pub in_await_arg: bool,
|
||||
|
||||
pub(super) is_delete_arg: bool,
|
||||
pub is_delete_arg: bool,
|
||||
|
||||
pub(super) in_left_of_for_loop: bool,
|
||||
pub in_left_of_for_loop: bool,
|
||||
|
||||
pub(super) executed_multiple_time: bool,
|
||||
pub executed_multiple_time: bool,
|
||||
/// Are we handling argument of an update expression.
|
||||
pub(super) in_update_arg: bool,
|
||||
pub(super) in_assign_lhs: bool,
|
||||
pub(super) in_cond: bool,
|
||||
pub in_update_arg: bool,
|
||||
pub in_assign_lhs: bool,
|
||||
pub in_cond: bool,
|
||||
|
||||
pub(super) inline_prevented: bool,
|
||||
pub inline_prevented: bool,
|
||||
|
||||
pub(super) is_op_assign: bool,
|
||||
pub is_op_assign: bool,
|
||||
}
|
||||
|
||||
pub(super) struct WithCtx<'a, S>
|
@ -1,51 +1,26 @@
|
||||
use std::{collections::HashSet, hash::BuildHasherDefault};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
|
||||
use swc_atoms::{js_word, JsWord};
|
||||
use swc_common::{
|
||||
collections::{AHashMap, AHashSet},
|
||||
SyntaxContext,
|
||||
};
|
||||
use swc_atoms::js_word;
|
||||
use swc_common::{collections::AHashMap, SyntaxContext};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_utils::{find_pat_ids, IsEmpty, StmtExt};
|
||||
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
||||
use swc_timer::timer;
|
||||
|
||||
use self::{
|
||||
ctx::Ctx,
|
||||
storage::{Storage, *},
|
||||
};
|
||||
pub use self::ctx::Ctx;
|
||||
use self::storage::{Storage, *};
|
||||
use crate::{
|
||||
alias::{collect_infects_from, Access, AccessKind, AliasConfig},
|
||||
alias::{collect_infects_from, AliasConfig},
|
||||
marks::Marks,
|
||||
util::can_end_conditionally,
|
||||
};
|
||||
|
||||
mod ctx;
|
||||
pub(crate) mod storage;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ModuleInfo {
|
||||
/// Imported identifiers which should be treated as a black box.
|
||||
///
|
||||
/// Imports from `@swc/helpers` are excluded as helpers are not modified by
|
||||
/// accessing/calling other modules.
|
||||
pub blackbox_imports: AHashSet<Id>,
|
||||
}
|
||||
|
||||
pub(crate) fn analyze<N>(n: &N, _module_info: &ModuleInfo, marks: Option<Marks>) -> ProgramData
|
||||
where
|
||||
N: VisitWith<UsageAnalyzer>,
|
||||
{
|
||||
analyze_with_storage::<ProgramData, _>(n, marks)
|
||||
}
|
||||
pub mod storage;
|
||||
|
||||
/// TODO: Track assignments to variables via `arguments`.
|
||||
/// TODO: Scope-local. (Including block)
|
||||
///
|
||||
/// If `marks` is [None], markers are ignored.
|
||||
pub(crate) fn analyze_with_storage<S, N>(n: &N, marks: Option<Marks>) -> S
|
||||
pub fn analyze_with_storage<S, N>(n: &N, marks: Option<Marks>) -> S
|
||||
where
|
||||
S: Storage,
|
||||
N: VisitWith<UsageAnalyzer<S>>,
|
||||
@ -68,292 +43,21 @@ where
|
||||
v.data
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct VarUsageInfo {
|
||||
pub inline_prevented: bool,
|
||||
|
||||
/// The number of direct reference to this identifier.
|
||||
pub ref_count: u32,
|
||||
|
||||
/// `true` if a variable is conditionally initialized.
|
||||
pub cond_init: bool,
|
||||
|
||||
/// `false` if it's only used.
|
||||
pub declared: bool,
|
||||
pub declared_count: u32,
|
||||
|
||||
/// `true` if the enclosing function defines this variable as a parameter.
|
||||
pub declared_as_fn_param: bool,
|
||||
|
||||
pub declared_as_fn_decl: bool,
|
||||
pub declared_as_fn_expr: bool,
|
||||
|
||||
pub assign_count: u32,
|
||||
pub mutation_by_call_count: u32,
|
||||
|
||||
/// The number of direct and indirect reference to this identifier.
|
||||
/// ## Things to note
|
||||
///
|
||||
/// - Update is counted as usage, but assign is not
|
||||
pub usage_count: u32,
|
||||
|
||||
/// The variable itself is modified.
|
||||
reassigned_with_assignment: bool,
|
||||
reassigned_with_var_decl: bool,
|
||||
/// The variable itself or a property of it is modified.
|
||||
pub mutated: bool,
|
||||
|
||||
pub has_property_access: bool,
|
||||
pub has_property_mutation: bool,
|
||||
|
||||
pub exported: bool,
|
||||
/// True if used **above** the declaration or in init. (Not eval order).
|
||||
pub used_above_decl: bool,
|
||||
/// `true` if it's declared by function parameters or variables declared in
|
||||
/// a closest function and used only within it and not used by child
|
||||
/// functions.
|
||||
pub is_fn_local: bool,
|
||||
|
||||
pub executed_multiple_time: bool,
|
||||
pub used_in_cond: bool,
|
||||
|
||||
pub var_kind: Option<VarDeclKind>,
|
||||
pub var_initialized: bool,
|
||||
|
||||
pub declared_as_catch_param: bool,
|
||||
|
||||
pub no_side_effect_for_member_access: bool,
|
||||
|
||||
pub callee_count: u32,
|
||||
|
||||
pub used_as_arg: bool,
|
||||
|
||||
pub indexed_with_dynamic_key: bool,
|
||||
|
||||
pub pure_fn: bool,
|
||||
|
||||
/// `infects_to`. This should be renamed, but it will be done with another
|
||||
/// PR. (because it's hard to review)
|
||||
infects: Vec<Access>,
|
||||
|
||||
pub used_in_non_child_fn: bool,
|
||||
/// Only **string** properties.
|
||||
pub accessed_props: Box<AHashMap<JsWord, u32>>,
|
||||
|
||||
pub used_recursively: bool,
|
||||
}
|
||||
|
||||
impl Default for VarUsageInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inline_prevented: Default::default(),
|
||||
ref_count: Default::default(),
|
||||
cond_init: Default::default(),
|
||||
declared: Default::default(),
|
||||
declared_count: Default::default(),
|
||||
declared_as_fn_param: Default::default(),
|
||||
declared_as_fn_decl: Default::default(),
|
||||
declared_as_fn_expr: Default::default(),
|
||||
assign_count: Default::default(),
|
||||
mutation_by_call_count: Default::default(),
|
||||
usage_count: Default::default(),
|
||||
reassigned_with_assignment: Default::default(),
|
||||
reassigned_with_var_decl: Default::default(),
|
||||
mutated: Default::default(),
|
||||
has_property_access: Default::default(),
|
||||
has_property_mutation: Default::default(),
|
||||
exported: Default::default(),
|
||||
used_above_decl: Default::default(),
|
||||
is_fn_local: true,
|
||||
executed_multiple_time: Default::default(),
|
||||
used_in_cond: Default::default(),
|
||||
var_kind: Default::default(),
|
||||
var_initialized: Default::default(),
|
||||
declared_as_catch_param: Default::default(),
|
||||
no_side_effect_for_member_access: Default::default(),
|
||||
callee_count: Default::default(),
|
||||
used_as_arg: Default::default(),
|
||||
indexed_with_dynamic_key: Default::default(),
|
||||
pure_fn: Default::default(),
|
||||
infects: Default::default(),
|
||||
used_in_non_child_fn: Default::default(),
|
||||
accessed_props: Default::default(),
|
||||
used_recursively: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VarUsageInfo {
|
||||
pub fn is_mutated_only_by_one_call(&self) -> bool {
|
||||
self.assign_count == 0 && self.mutation_by_call_count == 1
|
||||
}
|
||||
|
||||
pub fn is_infected(&self) -> bool {
|
||||
!self.infects.is_empty()
|
||||
}
|
||||
|
||||
pub fn reassigned(&self) -> bool {
|
||||
self.reassigned_with_assignment
|
||||
|| self.reassigned_with_var_decl
|
||||
|| (u32::from(self.var_initialized)
|
||||
+ u32::from(self.declared_as_catch_param)
|
||||
+ u32::from(self.declared_as_fn_param)
|
||||
+ self.assign_count)
|
||||
> 1
|
||||
}
|
||||
|
||||
pub fn can_inline_var(&self) -> bool {
|
||||
!self.mutated
|
||||
|| (self.assign_count == 0 && !self.reassigned() && !self.has_property_mutation)
|
||||
}
|
||||
|
||||
pub fn can_inline_fn_once(&self) -> bool {
|
||||
self.callee_count > 0
|
||||
|| !self.executed_multiple_time && (self.is_fn_local || !self.used_in_non_child_fn)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ScopeKind {
|
||||
pub enum ScopeKind {
|
||||
Fn,
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct ScopeData {
|
||||
pub has_with_stmt: bool,
|
||||
pub has_eval_call: bool,
|
||||
pub used_arguments: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum RecursiveUsage {
|
||||
FnOrClass,
|
||||
Var { can_ignore: bool },
|
||||
}
|
||||
|
||||
/// Analyzed info of a whole program we are working on.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct ProgramData {
|
||||
pub vars: FxHashMap<Id, VarUsageInfo>,
|
||||
|
||||
pub top: ScopeData,
|
||||
|
||||
pub scopes: FxHashMap<SyntaxContext, ScopeData>,
|
||||
|
||||
initialized_vars: IndexSet<Id, ahash::RandomState>,
|
||||
}
|
||||
|
||||
impl ProgramData {
|
||||
pub(crate) fn expand_infected(
|
||||
&self,
|
||||
module_info: &ModuleInfo,
|
||||
ids: FxHashSet<Access>,
|
||||
max_num: usize,
|
||||
) -> Result<FxHashSet<Access>, ()> {
|
||||
let init =
|
||||
HashSet::with_capacity_and_hasher(max_num, BuildHasherDefault::<FxHasher>::default());
|
||||
ids.into_iter().try_fold(init, |mut res, id| {
|
||||
let mut ids = Vec::with_capacity(max_num);
|
||||
ids.push(id);
|
||||
let mut ranges = vec![0..1usize];
|
||||
loop {
|
||||
let range = ranges.remove(0);
|
||||
for index in range {
|
||||
let iid = ids.get(index).unwrap();
|
||||
|
||||
// Abort on imported variables, because we can't analyze them
|
||||
if module_info.blackbox_imports.contains(&iid.0) {
|
||||
return Err(());
|
||||
}
|
||||
if !res.insert(iid.clone()) {
|
||||
continue;
|
||||
}
|
||||
if res.len() >= max_num {
|
||||
return Err(());
|
||||
}
|
||||
if let Some(info) = self.vars.get(&iid.0) {
|
||||
let infects = &info.infects;
|
||||
if !infects.is_empty() {
|
||||
let old_len = ids.len();
|
||||
|
||||
// This is not a call, so effects from call can be skipped
|
||||
let can_skip_non_call = matches!(iid.1, AccessKind::Reference)
|
||||
|| (info.declared_count == 1
|
||||
&& info.declared_as_fn_decl
|
||||
&& !info.reassigned());
|
||||
|
||||
if can_skip_non_call {
|
||||
ids.extend(
|
||||
infects
|
||||
.iter()
|
||||
.filter(|(_, kind)| *kind != AccessKind::Call)
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
ids.extend_from_slice(infects.as_slice());
|
||||
}
|
||||
let new_len = ids.len();
|
||||
ranges.push(old_len..new_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ranges.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn contains_unresolved(&self, e: &Expr) -> bool {
|
||||
match e {
|
||||
Expr::Ident(i) => {
|
||||
if let Some(v) = self.vars.get(&i.to_id()) {
|
||||
return !v.declared;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
Expr::Member(MemberExpr { obj, prop, .. }) => {
|
||||
if self.contains_unresolved(obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let MemberProp::Computed(prop) = prop {
|
||||
if self.contains_unresolved(&prop.expr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
Expr::Call(CallExpr {
|
||||
callee: Callee::Expr(callee),
|
||||
args,
|
||||
..
|
||||
}) => {
|
||||
if self.contains_unresolved(callee) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if args.iter().any(|arg| self.contains_unresolved(&arg.expr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This assumes there are no two variable with same name and same span hygiene.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UsageAnalyzer<S = ProgramData>
|
||||
pub struct UsageAnalyzer<S>
|
||||
where
|
||||
S: Storage,
|
||||
{
|
||||
@ -1504,10 +1208,8 @@ where
|
||||
} = e
|
||||
{
|
||||
// TODO: merge with may_have_side_effects
|
||||
let can_ignore = match &**init {
|
||||
Expr::Call(call) if call.span.has_mark(marks.pure) => true,
|
||||
_ => false,
|
||||
};
|
||||
let can_ignore =
|
||||
matches!(&**init, Expr::Call(call) if call.span.has_mark(marks.pure));
|
||||
let id = id.to_id();
|
||||
self.used_recursively
|
||||
.insert(id.clone(), RecursiveUsage::Var { can_ignore });
|
@ -5,9 +5,7 @@ use swc_ecma_ast::*;
|
||||
use super::{ctx::Ctx, ScopeKind};
|
||||
use crate::alias::Access;
|
||||
|
||||
pub mod normal;
|
||||
|
||||
pub(crate) trait Storage: Sized + Default {
|
||||
pub trait Storage: Sized + Default {
|
||||
type ScopeData: ScopeDataLike;
|
||||
type VarData: VarDataLike;
|
||||
|
||||
@ -33,7 +31,7 @@ pub(crate) trait Storage: Sized + Default {
|
||||
fn truncate_initialized_cnt(&mut self, len: usize);
|
||||
}
|
||||
|
||||
pub(crate) trait ScopeDataLike: Sized + Default + Clone {
|
||||
pub trait ScopeDataLike: Sized + Default + Clone {
|
||||
fn add_declared_symbol(&mut self, id: &Ident);
|
||||
|
||||
fn merge(&mut self, other: Self, is_child: bool);
|
||||
@ -45,7 +43,7 @@ pub(crate) trait ScopeDataLike: Sized + Default + Clone {
|
||||
fn mark_with_stmt(&mut self);
|
||||
}
|
||||
|
||||
pub(crate) trait VarDataLike: Sized {
|
||||
pub trait VarDataLike: Sized {
|
||||
/// See `declared_as_fn_param` of [crate::analyzer::VarUsageInfo].
|
||||
fn mark_declared_as_fn_param(&mut self);
|
||||
|
4
crates/swc_ecma_usage_analyzer/src/lib.rs
Normal file
4
crates/swc_ecma_usage_analyzer/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod alias;
|
||||
pub mod analyzer;
|
||||
pub mod marks;
|
||||
pub mod util;
|
@ -9,13 +9,13 @@ pub struct Marks {
|
||||
///
|
||||
/// In other words, AST nodes marked with this mark will not be treated as a
|
||||
/// top level item, even if it's in the top level scope.
|
||||
pub(crate) non_top_level: Mark,
|
||||
pub non_top_level: Mark,
|
||||
|
||||
/// Indicates that a sequence expression is generated by the minifier.
|
||||
///
|
||||
/// This is required because `sequences` option is ignored for synthesized
|
||||
/// sequences.
|
||||
pub(crate) synthesized_seq: Mark,
|
||||
pub synthesized_seq: Mark,
|
||||
|
||||
/// Treat this function as a top level module.
|
||||
///
|
||||
@ -29,25 +29,25 @@ pub struct Marks {
|
||||
///
|
||||
/// This is only applied to [swc_ecma_ast::Function] and it should not be
|
||||
/// nested.
|
||||
pub(crate) standalone: Mark,
|
||||
pub standalone: Mark,
|
||||
|
||||
//// Applied to [swc_ecma_ast::Module].
|
||||
pub(crate) bundle_of_standalone: Mark,
|
||||
pub bundle_of_standalone: Mark,
|
||||
|
||||
/// `/** @const */`.
|
||||
pub(crate) const_ann: Mark,
|
||||
pub const_ann: Mark,
|
||||
|
||||
/// Check for `/*#__NOINLINE__*/`
|
||||
pub(crate) noinline: Mark,
|
||||
pub noinline: Mark,
|
||||
|
||||
/// Check for `/*#__PURE__*/`
|
||||
pub(crate) pure: Mark,
|
||||
pub pure: Mark,
|
||||
|
||||
/// This is applied to [swc_ecma_ast::BlockStmt] which is injected to
|
||||
/// preserve the side effects.
|
||||
pub(crate) fake_block: Mark,
|
||||
pub fake_block: Mark,
|
||||
|
||||
pub(crate) unresolved_mark: Mark,
|
||||
pub unresolved_mark: Mark,
|
||||
}
|
||||
|
||||
impl Marks {
|
90
crates/swc_ecma_usage_analyzer/src/util.rs
Normal file
90
crates/swc_ecma_usage_analyzer/src/util.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use swc_atoms::{js_word, JsWord};
|
||||
use swc_ecma_ast::Stmt;
|
||||
|
||||
pub fn is_global_var_with_pure_property_access(s: &JsWord) -> bool {
|
||||
match *s {
|
||||
js_word!("JSON")
|
||||
| js_word!("Array")
|
||||
| js_word!("String")
|
||||
| js_word!("Object")
|
||||
| js_word!("Number")
|
||||
| js_word!("Date")
|
||||
| js_word!("BigInt")
|
||||
| js_word!("Boolean")
|
||||
| js_word!("Math")
|
||||
| js_word!("Error") => return true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
matches!(
|
||||
&**s,
|
||||
"console"
|
||||
| "clearInterval"
|
||||
| "clearTimeout"
|
||||
| "setInterval"
|
||||
| "setTimeout"
|
||||
| "btoa"
|
||||
| "decodeURI"
|
||||
| "decodeURIComponent"
|
||||
| "encodeURI"
|
||||
| "encodeURIComponent"
|
||||
| "escape"
|
||||
| "eval"
|
||||
| "EvalError"
|
||||
| "Function"
|
||||
| "isFinite"
|
||||
| "isNaN"
|
||||
| "JSON"
|
||||
| "parseFloat"
|
||||
| "parseInt"
|
||||
| "RegExp"
|
||||
| "RangeError"
|
||||
| "ReferenceError"
|
||||
| "SyntaxError"
|
||||
| "TypeError"
|
||||
| "unescape"
|
||||
| "URIError"
|
||||
| "atob"
|
||||
| "globalThis"
|
||||
| "NaN"
|
||||
| "Symbol"
|
||||
| "Promise"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn can_end_conditionally(s: &Stmt) -> bool {
|
||||
///
|
||||
///`ignore_always`: If true, [Stmt::Return] will be ignored.
|
||||
fn can_end(s: &Stmt, ignore_always: bool) -> bool {
|
||||
match s {
|
||||
Stmt::If(s) => {
|
||||
can_end(&s.cons, false)
|
||||
|| s.alt
|
||||
.as_deref()
|
||||
.map(|s| can_end(s, false))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
Stmt::Switch(s) => s
|
||||
.cases
|
||||
.iter()
|
||||
.any(|case| case.cons.iter().any(|s| can_end(s, false))),
|
||||
|
||||
Stmt::DoWhile(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::While(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::For(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::ForOf(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::ForIn(s) => can_end(&s.body, false),
|
||||
|
||||
Stmt::Return(..) | Stmt::Break(..) | Stmt::Continue(..) => !ignore_always,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
can_end(s, true)
|
||||
}
|
Loading…
Reference in New Issue
Block a user