feat(linter/tree-shaking): support part BinaryExpression (#2922)

This commit is contained in:
Wang Wenzhe 2024-04-09 12:33:51 +08:00 committed by GitHub
parent 659e891615
commit 990eda61d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 133 additions and 30 deletions

View file

@ -3,11 +3,11 @@ use std::cell::{Cell, RefCell};
use oxc_ast::{
ast::{
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentTarget,
BindingPattern, BindingPatternKind, CallExpression, ComputedMemberExpression, Declaration,
Expression, FormalParameter, Function, IdentifierReference, MemberExpression,
ModuleDeclaration, NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program,
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
VariableDeclarator,
BinaryExpression, BindingPattern, BindingPatternKind, CallExpression,
ComputedMemberExpression, Declaration, Expression, FormalParameter, Function,
IdentifierReference, MemberExpression, ModuleDeclaration, NewExpression,
ParenthesizedExpression, PrivateFieldExpression, Program, SimpleAssignmentTarget,
Statement, StaticMemberExpression, ThisExpression, VariableDeclarator,
},
AstKind,
};
@ -17,7 +17,7 @@ use rustc_hash::FxHashSet;
use crate::{
ast_util::{get_declaration_of_variable, get_symbol_id_of_variable},
utils::{get_write_expr, has_pure_notation, no_effects, Value},
utils::{calculate_binary_operation, get_write_expr, has_pure_notation, no_effects, Value},
LintContext,
};
@ -51,8 +51,8 @@ pub trait ListenerMap {
fn report_effects_when_assigned(&self, _options: &NodeListenerOptions) {}
fn report_effects_when_called(&self, _options: &NodeListenerOptions) {}
fn report_effects_when_mutated(&self, _options: &NodeListenerOptions) {}
fn get_value_and_report_effects(&self, _options: &NodeListenerOptions) -> Option<Value> {
None
fn get_value_and_report_effects(&self, _options: &NodeListenerOptions) -> Value {
Value::Unknown
}
}
@ -97,6 +97,9 @@ impl<'a> ListenerMap for Statement<'a> {
finalizer.body.iter().for_each(|stmt| stmt.report_effects(options));
});
}
Self::BlockStatement(stmt) => {
stmt.body.iter().for_each(|stmt| stmt.report_effects(options));
}
_ => {}
}
}
@ -224,6 +227,9 @@ impl<'a> ListenerMap for Expression<'a> {
Self::AwaitExpression(expr) => {
expr.argument.report_effects(options);
}
Self::BinaryExpression(expr) => {
expr.get_value_and_report_effects(options);
}
Self::ArrowFunctionExpression(_)
| Self::FunctionExpression(_)
| Self::Identifier(_)
@ -281,6 +287,18 @@ impl<'a> ListenerMap for Expression<'a> {
}
}
}
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
match self {
Self::BooleanLiteral(_)
| Self::StringLiteral(_)
| Self::NumericLiteral(_)
| Self::TemplateLiteral(_) => Value::new(self),
_ => {
self.report_effects(options);
Value::Unknown
}
}
}
}
// which kind of Expression defines `report_effects_when_called` method.
@ -297,6 +315,14 @@ fn defined_custom_report_effects_when_called(expr: &Expression) -> bool {
)
}
impl<'a> ListenerMap for BinaryExpression<'a> {
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
let left = self.left.get_value_and_report_effects(options);
let right = self.right.get_value_and_report_effects(options);
calculate_binary_operation(self.operator, left, right)
}
}
impl ListenerMap for ThisExpression {
fn report_effects_when_mutated(&self, options: &NodeListenerOptions) {
if !options.has_valid_this.get() {
@ -331,7 +357,7 @@ impl<'a> ListenerMap for ParenthesizedExpression<'a> {
fn report_effects_when_mutated(&self, options: &NodeListenerOptions) {
self.expression.report_effects_when_mutated(options);
}
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Option<Value> {
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
self.expression.get_value_and_report_effects(options)
}
}

View file

@ -99,19 +99,19 @@ fn test() {
"const x = []",
"const x = [ext,ext]",
"const x = [1,,2,]",
// // ArrayPattern
// ArrayPattern
"const [x] = []",
"const [,x,] = []",
// // ArrowFunctionExpression
// ArrowFunctionExpression
"const x = a=>{a(); ext()}",
// // ArrowFunctionExpression when called
// ArrowFunctionExpression when called
"(()=>{})()",
"(a=>{})()",
"((...a)=>{})()",
"(({a})=>{})()",
// // ArrowFunctionExpression when mutated
// ArrowFunctionExpression when mutated
"const x = ()=>{}; x.y = 1",
// // AssignmentExpression
// AssignmentExpression
"var x;x = {}",
"var x;x += 1",
"const x = {}; x.y = 1",
@ -124,19 +124,19 @@ fn test() {
"const {x: y = ext} = {}",
"const {[ext]: x = ext} = {}",
"const x = ()=>{}, {y = x()} = {}",
// // BinaryExpression
// "const x = 1 + 2",
// "if (1-1) ext()",
// // BlockStatement
// "{}",
// "const x = ()=>{};{const x = ext}x()",
// "const x = ext;{const x = ()=>{}; x()}",
// // BreakStatement
// "while(true){break}",
// // CallExpression
// BinaryExpression
"const x = 1 + 2",
"if (1-1) ext()",
// BlockStatement
"{}",
"const x = ()=>{};{const x = ext}x()",
"const x = ext;{const x = ()=>{}; x()}",
// BreakStatement
"while(true){break}",
// CallExpression
"(a=>{const y = a})(ext, ext)",
"const x = ()=>{}, y = ()=>{}; x(y())",
// // CatchClause
// CatchClause
"try {} catch (error) {}",
"const x = ()=>{}; try {} catch (error) {const x = ext}; x()",
"const x = ext; try {} catch (error) {const x = ()=>{}; x()}",
@ -393,12 +393,12 @@ fn test() {
// AwaitExpression
"const x = async ()=>{await ext()}; x()",
// // BinaryExpression
// "const x = 1 + ext()",
// "const x = ext() + 1",
// // BlockStatement
// "{ext()}",
"const x = 1 + ext()",
"const x = ext() + 1",
// BlockStatement
"{ext()}",
// "var x=()=>{};{var x=ext}x()",
// "var x=ext;{x(); var x=()=>{}}",
"var x=ext;{x(); var x=()=>{}}",
// CallExpression
"(()=>{})(ext(), 1)",
"(()=>{})(1, ext())",

View file

@ -128,6 +128,30 @@ expression: no_side_effects_in_initialization
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext`
╭─[no_side_effects_in_initialization.tsx:1:15]
1 │ const x = 1 + ext()
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext`
╭─[no_side_effects_in_initialization.tsx:1:11]
1 │ const x = ext() + 1
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext`
╭─[no_side_effects_in_initialization.tsx:1:2]
1 │ {ext()}
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext`
╭─[no_side_effects_in_initialization.tsx:1:7]
1 │ var x=ext;{x(); var x=()=>{}}
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext`
╭─[no_side_effects_in_initialization.tsx:1:10]
1 │ (()=>{})(ext(), 1)

View file

@ -1,6 +1,7 @@
use oxc_ast::{ast::Expression, AstKind};
use oxc_semantic::AstNodeId;
use oxc_span::Span;
use oxc_syntax::operator::BinaryOperator;
use crate::LintContext;
@ -8,6 +9,40 @@ use crate::LintContext;
pub enum Value {
Boolean(bool),
Number(f64),
String(StringValue),
Unknown,
}
// We only care if it is falsy value (empty string).
pub enum StringValue {
Empty,
NonEmpty,
}
impl Value {
pub fn new(expr: &Expression) -> Value {
match expr {
Expression::BooleanLiteral(bool_lit) => Value::Boolean(bool_lit.value),
Expression::NumericLiteral(num_lit) => Value::Number(num_lit.value),
Expression::StringLiteral(str_lit) => {
if str_lit.value.is_empty() {
Value::String(StringValue::Empty)
} else {
Value::String(StringValue::NonEmpty)
}
}
Expression::TemplateLiteral(template_lit) => {
if !template_lit.is_no_substitution_template() {
Value::Unknown
} else if template_lit.quasi().is_some_and(|s| s == "") {
Value::String(StringValue::Empty)
} else {
Value::String(StringValue::NonEmpty)
}
}
_ => Value::Unknown,
}
}
}
pub fn get_write_expr<'a, 'b>(
@ -43,3 +78,21 @@ pub fn has_pure_notation(span: Span, ctx: &LintContext) -> bool {
raw.contains("@__PURE__") || raw.contains("#__PURE__")
}
/// Port from https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/463fa1f0bef7caa2b231a38b9c3557051f506c92/src/rules/no-side-effects-in-initialization.ts#L136-L161
pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value) -> Value {
match op {
BinaryOperator::Addition => match (left, right) {
(Value::Number(a), Value::Number(b)) => Value::Number(a + b),
(Value::String(str1), Value::String(str2)) => {
if matches!(str1, StringValue::Empty) && matches!(str2, StringValue::Empty) {
Value::String(StringValue::Empty)
} else {
Value::String(StringValue::NonEmpty)
}
}
_ => Value::Unknown,
},
_ => Value::Unknown,
}
}