-
-
Notifications
You must be signed in to change notification settings - Fork 757
feat: support optimize side effects free function calls #12559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for rspack ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Rsdoctor Bundle Diff AnalysisFound 5 projects in monorepo, 0 projects with changes. 📊 Quick Summary
Generated by Rsdoctor GitHub Action |
📦 Binary Size-limit
❌ Size increased by 70.38KB from 47.88MB to 47.95MB (⬆️0.14%) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for the #__NO_SIDE_EFFECTS__ notation from the JavaScript Compiler Hints specification, enabling better tree-shaking optimization. The feature allows developers to mark function definitions as pure (having no side effects), which Rspack can use to safely eliminate unused function calls.
Key changes:
- Implements
#__NO_SIDE_EFFECTS__comment annotation parsing for various function types (declarations, expressions, arrows, exports) - Adds
pureFunctionsconfiguration option in module rules to manually flag functions as pure for third-party code - Introduces deferred purity checking system that validates imported functions across module boundaries
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| website/docs/en/guide/optimization/tree-shaking.mdx | Adds English documentation for NO_SIDE_EFFECTS notation and pureFunctions config |
| website/docs/zh/guide/optimization/tree-shaking.mdx | Adds Chinese documentation for the new feature |
| tests/rspack-test/configCases/tree-shaking/no-side-effects-notation/* | Test cases for NO_SIDE_EFFECTS annotation on various function types |
| tests/rspack-test/configCases/tree-shaking/user-mark-pure-functions/* | Test cases for pureFunctions config and warning validation |
| packages/rspack/src/config/types.ts | Adds pureFunctions array type to JavascriptParserOptions |
| packages/rspack/src/config/normalization.ts | Adds noSideEffectsNotation to ExperimentsNormalized |
| packages/rspack/src/config/defaults.ts | Sets default value (false) for noSideEffectsNotation experiment |
| packages/rspack/src/config/adapter.ts | Maps pureFunctions config to raw options |
| packages/rspack/etc/core.api.md | Updates API documentation with new config options |
| crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs | Implements annotation parsing, deferred checking, and validation warnings |
| crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs | Adds finish_modules hook to validate deferred pure checks and unused ParsedPureFunction struct |
| crates/rspack_plugin_javascript/src/plugin/flag_dependency_exports_plugin.rs | Exports stage constant for plugin ordering |
| crates/rspack_plugin_javascript/src/parser_plugin/inner_graph/plugin.rs | Threads analyze_pure_annotation parameter through to purity checking functions |
| crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs | Changes definitions field visibility to pub(crate) for cross-module access |
| crates/rspack_core/src/module.rs | Adds pure_functions and deferred_pure_checks to BuildInfo |
| crates/rspack_core/src/options/module.rs | Adds pureFunctions option to JavascriptParserOptions |
| crates/rspack_core/src/options/experiments/mod.rs | Adds no_side_effects_notation flag to Experiments |
| crates/rspack_binding_api/src/raw_options/raw_module/mod.rs | Binds pureFunctions from JavaScript to Rust |
| crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs | Binds noSideEffectsNotation from JavaScript to Rust |
| crates/rspack/src/builder/mod.rs | Adds builder support for noSideEffectsNotation experiment |
| crates/rspack/src/builder/builder_context.rs | Minor whitespace formatting |
| crates/node_binding/napi-binding.d.ts | Updates TypeScript type definitions |
Comments suppressed due to low confidence (1)
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs:843
- The
analyze_pure_notationparameter is added to numerous functions (is_pure_call_expr,is_pure_expression,is_pure_class,is_pure_decl,is_pure_var_decl,is_pure_class_member,is_pure_pat,is_pure_function) but is never actually used to control any conditional logic within these functions. This suggests the parameter is unnecessary code that should either be removed or should actually gate some behavior related to analyzing pure notations.
fn is_pure_call_expr(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
expr: &Expr,
unresolved_ctxt: SyntaxContext,
comments: Option<&dyn Comments>,
) -> bool {
let Expr::Call(call_expr) = expr else {
unreachable!();
};
let callee = &call_expr.callee;
let pure_flag = comments
.map(|comments| comments.has_flag(callee.span().lo, "PURE"))
.unwrap_or(false);
if pure_flag {
call_expr.args.iter().all(|arg| {
if arg.spread.is_some() {
false
} else {
is_pure_expression(
parser,
analyze_pure_notation,
&arg.expr,
unresolved_ctxt,
comments,
)
}
})
} else {
if let Some(Expr::Ident(ident)) = callee.as_expr().map(|expr| expr.as_ref())
&& parser
.build_info
.pure_functions
.as_ref()
.map(|pure_functions| pure_functions.contains(&ident.sym))
.unwrap_or(false)
{
// this is a locally pure function
return true;
}
if let Some(deferred_check) = try_extract_deferred_check(parser, callee) {
parser.build_info.deferred_pure_checks.push(deferred_check);
return call_expr.args.iter().all(|arg| {
if arg.spread.is_some() {
false
} else {
is_pure_expression(
parser,
analyze_pure_notation,
&arg.expr,
unresolved_ctxt,
comments,
)
}
});
}
!expr.may_have_side_effects(ExprCtx {
unresolved_ctxt,
in_strict: false,
is_unresolved_ref_safe: false,
remaining_depth: 4,
})
}
}
fn try_extract_deferred_check(
parser: &mut JavascriptParser,
callee: &Callee,
) -> Option<DeferredPureCheck> {
let Callee::Expr(expr) = callee else {
return None;
};
let info = match &**expr {
Expr::Ident(ident) => parser.get_variable_info(&ident.sym)?,
_ => return None,
};
let tag_info_id = info.tag_info?;
let tag_info = parser.definitions_db.expect_get_tag_info(tag_info_id);
if tag_info.tag != ESM_SPECIFIER_TAG {
return None;
}
let data = ESMSpecifierData::downcast(tag_info.data.clone()?);
Some(DeferredPureCheck {
import_request: data.source.to_string(),
atom: data.name.clone(),
start: callee.span().lo.0,
end: callee.span().hi.0,
})
}
impl SideEffectsParserPlugin {
fn analyze_stmt_side_effects(&self, stmt: &Statement, parser: &mut JavascriptParser) {
if parser.side_effects_item.is_some() {
return;
}
match stmt {
Statement::If(if_stmt) => {
if !is_pure_expression(
parser,
self.analyze_pure_notation,
&if_stmt.test,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
if_stmt.span(),
String::from("Statement"),
));
}
}
Statement::While(while_stmt) => {
if !is_pure_expression(
parser,
self.analyze_pure_notation,
&while_stmt.test,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
while_stmt.span(),
String::from("Statement"),
));
}
}
Statement::DoWhile(do_while_stmt) => {
if !is_pure_expression(
parser,
self.analyze_pure_notation,
&do_while_stmt.test,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
do_while_stmt.span(),
String::from("Statement"),
));
}
}
Statement::For(for_stmt) => {
let pure_init = match for_stmt.init {
Some(ref init) => match init {
VarDeclOrExpr::VarDecl(decl) => is_pure_var_decl(
parser,
self.analyze_pure_notation,
decl,
self.unresolve_ctxt,
parser.comments,
),
VarDeclOrExpr::Expr(expr) => is_pure_expression(
parser,
self.analyze_pure_notation,
expr,
self.unresolve_ctxt,
parser.comments,
),
},
None => true,
};
if !pure_init {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
for_stmt.span(),
String::from("Statement"),
));
return;
}
let pure_test = match &for_stmt.test {
Some(test) => is_pure_expression(
parser,
self.analyze_pure_notation,
test,
self.unresolve_ctxt,
parser.comments,
),
None => true,
};
if !pure_test {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
for_stmt.span(),
String::from("Statement"),
));
return;
}
let pure_update = match for_stmt.update {
Some(ref expr) => is_pure_expression(
parser,
self.analyze_pure_notation,
expr,
self.unresolve_ctxt,
parser.comments,
),
None => true,
};
if !pure_update {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
for_stmt.span(),
String::from("Statement"),
));
}
}
Statement::Expr(expr_stmt) => {
if !is_pure_expression(
parser,
self.analyze_pure_notation,
&expr_stmt.expr,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
expr_stmt.span(),
String::from("Statement"),
));
}
}
Statement::Switch(switch_stmt) => {
if !is_pure_expression(
parser,
self.analyze_pure_notation,
&switch_stmt.discriminant,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
switch_stmt.span(),
String::from("Statement"),
));
}
}
Statement::Class(class_stmt) => {
if !is_pure_class(
parser,
self.analyze_pure_notation,
class_stmt.class(),
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
class_stmt.span(),
String::from("Statement"),
));
}
}
Statement::Var(var_stmt) => match var_stmt {
VariableDeclaration::VarDecl(var_decl) => {
if !is_pure_var_decl(
parser,
self.analyze_pure_notation,
var_decl,
self.unresolve_ctxt,
parser.comments,
) {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
var_stmt.span(),
String::from("Statement"),
));
}
}
VariableDeclaration::UsingDecl(_) => {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
var_stmt.span(),
String::from("Statement"),
));
}
},
Statement::Empty(_) => {}
Statement::Labeled(_) => {}
Statement::Block(_) => {}
Statement::Fn(_) => {}
_ => {
parser.side_effects_item = Some(SideEffectsBailoutItemWithSpan::new(
stmt.span(),
String::from("Statement"),
))
}
};
}
}
pub fn is_pure_pat<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
pat: &'a Pat,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
match pat {
Pat::Ident(_) => true,
Pat::Array(array_pat) => array_pat.elems.iter().all(|ele| {
if let Some(pat) = ele {
is_pure_pat(
parser,
analyze_pure_notation,
pat,
unresolved_ctxt,
comments,
)
} else {
true
}
}),
Pat::Rest(_) => true,
Pat::Invalid(_) | Pat::Assign(_) | Pat::Object(_) => false,
Pat::Expr(expr) => is_pure_expression(
parser,
analyze_pure_notation,
expr,
unresolved_ctxt,
comments,
),
}
}
pub fn is_pure_function<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
function: &'a Function,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
if !function.params.iter().all(|param| {
is_pure_pat(
parser,
analyze_pure_notation,
¶m.pat,
unresolved_ctxt,
comments,
)
}) {
return false;
}
true
}
pub fn is_pure_expression<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
expr: &'a Expr,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
pub fn _is_pure_expression<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
expr: &'a Expr,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
let drive = parser.plugin_drive.clone();
if let Some(res) = drive.is_pure(parser, expr) {
return res;
}
match expr {
Expr::Call(_) => is_pure_call_expr(
parser,
analyze_pure_notation,
expr,
unresolved_ctxt,
comments,
),
Expr::Paren(_) => unreachable!(),
Expr::Seq(seq_expr) => seq_expr.exprs.iter().all(|expr| {
is_pure_expression(
parser,
analyze_pure_notation,
expr,
unresolved_ctxt,
comments,
)
}),
_ => !expr.may_have_side_effects(ExprCtx {
unresolved_ctxt,
is_unresolved_ref_safe: true,
in_strict: false,
remaining_depth: 4,
}),
}
}
_is_pure_expression(
parser,
analyze_pure_notation,
expr,
unresolved_ctxt,
comments,
)
}
pub fn is_pure_class_member<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
member: &'a ClassMember,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
let is_key_pure = match member.class_key() {
Some(PropName::Ident(_ident)) => true,
Some(PropName::Str(_)) => true,
Some(PropName::Num(_)) => true,
Some(PropName::Computed(computed)) => is_pure_expression(
parser,
analyze_pure_notation,
&computed.expr,
unresolved_ctxt,
comments,
),
Some(PropName::BigInt(_)) => true,
None => true,
};
if !is_key_pure {
return false;
}
let is_static = member.is_static();
let is_value_pure = match member {
ClassMember::Constructor(_) => true,
ClassMember::Method(_) => true,
ClassMember::PrivateMethod(_) => true,
ClassMember::ClassProp(prop) => {
if let Some(ref value) = prop.value {
is_pure_expression(
parser,
analyze_pure_notation,
value,
unresolved_ctxt,
comments,
)
} else {
true
}
}
ClassMember::PrivateProp(prop) => {
if let Some(ref value) = prop.value {
is_pure_expression(
parser,
analyze_pure_notation,
value,
unresolved_ctxt,
comments,
)
} else {
true
}
}
ClassMember::TsIndexSignature(_) => unreachable!(),
ClassMember::Empty(_) => true,
ClassMember::StaticBlock(_) => false,
ClassMember::AutoAccessor(_) => false,
};
if is_static && !is_value_pure {
return false;
}
true
}
pub fn is_pure_decl(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
stmt: &Decl,
unresolved_ctxt: SyntaxContext,
comments: Option<&dyn Comments>,
) -> bool {
match stmt {
Decl::Class(class) => is_pure_class(
parser,
analyze_pure_notation,
&class.class,
unresolved_ctxt,
comments,
),
Decl::Fn(_) => true,
Decl::Var(var) => is_pure_var_decl(
parser,
analyze_pure_notation,
var,
unresolved_ctxt,
comments,
),
Decl::Using(_) => false,
Decl::TsInterface(_) => unreachable!(),
Decl::TsTypeAlias(_) => unreachable!(),
Decl::TsEnum(_) => unreachable!(),
Decl::TsModule(_) => unreachable!(),
}
}
pub fn is_pure_class(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
class: &Class,
unresolved_ctxt: SyntaxContext,
comments: Option<&dyn Comments>,
) -> bool {
if let Some(ref super_class) = class.super_class
&& !is_pure_expression(
parser,
analyze_pure_notation,
super_class,
unresolved_ctxt,
comments,
)
{
return false;
}
let is_pure_key = |parser: &mut JavascriptParser, key: &PropName| -> bool {
match key {
PropName::BigInt(_) | PropName::Ident(_) | PropName::Str(_) | PropName::Num(_) => true,
PropName::Computed(computed) => is_pure_expression(
parser,
analyze_pure_notation,
&computed.expr,
unresolved_ctxt,
comments,
),
}
};
class.body.iter().all(|item| -> bool {
match item {
ClassMember::Constructor(_) => class.super_class.is_none(),
ClassMember::Method(method) => is_pure_key(parser, &method.key),
ClassMember::PrivateMethod(method) => is_pure_expression(
parser,
analyze_pure_notation,
&Expr::PrivateName(method.key.clone()),
unresolved_ctxt,
comments,
),
ClassMember::ClassProp(prop) => {
is_pure_key(parser, &prop.key)
&& (!prop.is_static
|| if let Some(ref value) = prop.value {
is_pure_expression(
parser,
analyze_pure_notation,
value,
unresolved_ctxt,
comments,
)
} else {
true
})
}
ClassMember::PrivateProp(prop) => {
is_pure_expression(
parser,
analyze_pure_notation,
&Expr::PrivateName(prop.key.clone()),
unresolved_ctxt,
comments,
) && (!prop.is_static
|| if let Some(ref value) = prop.value {
is_pure_expression(
parser,
analyze_pure_notation,
value,
unresolved_ctxt,
comments,
)
} else {
true
})
}
ClassMember::TsIndexSignature(_) => unreachable!(),
ClassMember::Empty(_) => true,
ClassMember::StaticBlock(_) => false, // TODO: support is pure analyze for statements
ClassMember::AutoAccessor(_) => false,
}
})
}
fn is_pure_var_decl<'a>(
parser: &mut JavascriptParser,
analyze_pure_notation: bool,
var: &'a VarDecl,
unresolved_ctxt: SyntaxContext,
comments: Option<&'a dyn Comments>,
) -> bool {
var.decls.iter().all(|decl| {
if let Some(ref init) = decl.init {
is_pure_expression(
parser,
analyze_pure_notation,
init,
unresolved_ctxt,
comments,
)
} else {
true
}
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs
Outdated
Show resolved
Hide resolved
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs
Outdated
Show resolved
Hide resolved
crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs
Outdated
Show resolved
Hide resolved
crates/rspack_plugin_javascript/src/plugin/side_effects_flag_plugin.rs
Outdated
Show resolved
Hide resolved
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs
Outdated
Show resolved
Hide resolved
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs
Outdated
Show resolved
Hide resolved
tests/rspack-test/configCases/tree-shaking/user-mark-pure-functions/index.js
Outdated
Show resolved
Hide resolved
aa5c920 to
ed90abe
Compare
54392cb to
0d2f8a6
Compare
Merging this PR will degrade performance by 36.51%Summary
Performance Changes
Comparing Footnotes
|
2af18ae to
ac6765f
Compare
4eb6cfe to
9bc3ebc
Compare
5898d5c to
d48437b
Compare
5d7e734 to
4f6ecd8
Compare
|
📝 Benchmark detail: Open
|
| /// @experimental | ||
| pub jsx: Option<bool>, | ||
| pub defer_import: Option<bool>, | ||
| pub side_effects_free: Option<Vec<String>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to no_side_effects? since the annotation is #NO_SIDE_EFFECTS#
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking that this can be used in more common way in the future, for example member access a.b, so I prefer this name more generic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good.
Suggestion about naming: side_effects and side_effects_free is our core model, so I suggest for our core code using side_effects_free_xxx and side_effects_xxx for naming, and for user-level config, use pure/no_side_effects (more common at community) for naming
| dependency::{ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency}, | ||
| }; | ||
|
|
||
| pub static SIDE_EFFECTS_FLAG_PLUGIN_STAGE: i32 = FLAG_DEPENDENCY_EXPORTS_STAGE + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| pub static SIDE_EFFECTS_FLAG_PLUGIN_STAGE: i32 = FLAG_DEPENDENCY_EXPORTS_STAGE + 1; | |
| pub static SIDE_EFFECTS_FLAG_PLUGIN_STAGE: i32 = FLAG_DEPENDENCY_EXPORTS_STAGE + 10; |
| deferImport?: boolean; | ||
|
|
||
| /** Flag the function to have no side effects */ | ||
| sideEffectsFree?: string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| sideEffectsFree?: string[]; | |
| noSideEffects?: string[]; |
| if parser.side_effects_item.is_none() { | ||
| for (callee, span) in callees { | ||
| if let Some(deferred) = try_extract_deferred_check(parser, callee, span) { | ||
| parser.build_info.deferred_pure_checks.insert(deferred); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deferred is confusing, maybe rename to maybe_side_effects_free?
| parser.build_info.deferred_pure_checks.insert(deferred); | |
| parser.build_info.maybe_side_effects_free.insert(deferred); |
|
|
||
| let logger = compilation.get_logger("rspack.SideEffectsFlagPlugin"); | ||
|
|
||
| let modules_with_impurities = modules |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let modules_with_impurities = modules | |
| let maybe_side_effects_free_modules = modules |
Summary
Support
#__NO_SIDE_EFFECTS__notation and user flag side effects free function calls.What is it ?
it's a notation from https://github.com/javascript-compiler-hints/compiler-notations-spec/blob/main/no-side-effects-notation-spec.md, and it's already implemented by both esbuild and Rollup.
It can help Rspack analyze if one function is pure.
Integration
✅ Supports
#__NO_SIDE_EFFECTS__for following cases:Rspack will analyze all modules to find if the function is pure in its declaration module.
node_modulesUsed exports
Currently the pure function analyze is just work for
optimization.sideEffect, notoptimization.innerGraph, which means the call for pure function is still exist in outputRelated links
Checklist