feat(linter/tree-shaking): support DoWhileStatement and IfStatement (#2994)

This commit is contained in:
Wang Wenzhe 2024-04-16 09:44:29 +08:00 committed by GitHub
parent fd5002bc51
commit ac37d55600
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 165 additions and 31 deletions

View file

@ -104,8 +104,8 @@ impl<'a> ListenerMap for Statement<'a> {
Self::IfStatement(stmt) => {
let test_result = stmt.test.get_value_and_report_effects(options);
if let Some(falsy) = test_result.get_falsy_value() {
if falsy {
if let Some(is_falsy) = test_result.get_falsy_value() {
if is_falsy {
if let Some(alternate) = &stmt.alternate {
alternate.report_effects(options);
}
@ -119,6 +119,20 @@ impl<'a> ListenerMap for Statement<'a> {
}
}
}
Self::DoWhileStatement(stmt) => {
if stmt
.test
.get_value_and_report_effects(options)
.get_falsy_value()
.is_some_and(|is_falsy| is_falsy)
{
return;
}
stmt.body.report_effects(options);
}
Self::DebuggerStatement(stmt) => {
options.ctx.diagnostic(NoSideEffectsDiagnostic::Debugger(stmt.span));
}
_ => {}
}
}
@ -435,8 +449,8 @@ impl<'a> ListenerMap for ConditionalExpression<'a> {
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
let test_result = self.test.get_value_and_report_effects(options);
if let Some(falsy) = test_result.get_falsy_value() {
if falsy {
if let Some(is_falsy) = test_result.get_falsy_value() {
if is_falsy {
self.alternate.get_value_and_report_effects(options)
} else {
self.consequent.get_value_and_report_effects(options)

View file

@ -53,6 +53,10 @@ enum NoSideEffectsDiagnostic {
#[error("eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling function parameter")]
#[diagnostic(severity(warning))]
CallParameter(#[label] Span),
#[error("eslint-plugin-tree-shaking(no-side-effects-in-initialization): Debugger statements are side-effects")]
#[diagnostic(severity(warning))]
Debugger(#[label] Span),
}
/// <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/master/src/rules/no-side-effects-in-initialization.ts>
@ -171,12 +175,12 @@ fn test() {
"const x = ()=>{}; (false ? ext : x)()",
// ContinueStatement
"while(true){continue}",
// // DoWhileStatement
// "do {} while(true)",
// "do {} while(ext > 0)",
// "const x = ()=>{}; do x(); while(true)",
// // EmptyStatement
// ";",
// DoWhileStatement
"do {} while(true)",
"do {} while(ext > 0)",
"const x = ()=>{}; do x(); while(true)",
// EmptyStatement
";",
// // ExportAllDeclaration
// r#"export * from "import""#,
// // ExportDefaultDeclaration
@ -231,9 +235,9 @@ fn test() {
// // Identifier when mutated
// "const x = {}; x.y = ext",
// // IfStatement
// "let y;if (ext > 0) {y = 1} else {y = 2}",
// "if (false) {ext()}",
// "if (true) {} else {ext()}",
"let y;if (ext > 0) {y = 1} else {y = 2}",
"if (false) {ext()}",
"if (true) {} else {ext()}",
// // ImportDeclaration
// r#"import "import""#,
// r#"import x from "import-default""#,
@ -442,16 +446,16 @@ fn test() {
"const x = ext ? ext() : 2",
"const x = ext ? 1 : ext()",
"if (false ? false : true) ext()",
// // ConditionalExpression when called
// "const x = ()=>{}; (true ? ext : x)()",
// "const x = ()=>{}; (false ? x : ext)()",
// "const x = ()=>{}; (ext ? x : ext)()",
// // DebuggerStatement
// "debugger",
// // DoWhileStatement
// "do {} while(ext())",
// "do ext(); while(true)",
// "do {ext()} while(true)",
// ConditionalExpression when called
"const x = ()=>{}; (true ? ext : x)()",
"const x = ()=>{}; (false ? x : ext)()",
"const x = ()=>{}; (ext ? x : ext)()",
// DebuggerStatement
"debugger",
// DoWhileStatement
"do {} while(ext())",
"do ext(); while(true)",
"do {ext()} while(true)",
// // ExportDefaultDeclaration
// "export default ext()",
// "export default /* tree-shaking no-side-effects-when-called */ ext",
@ -523,11 +527,11 @@ fn test() {
// "var x = {}; var x = ext; x.y = 1",
// "var x = {}; x = ext; x.y = 1; x.y = 1; x.y = 1",
// "const x = {y:ext}; const {y} = x; y.z = 1",
// // IfStatement
// "if (ext()>0){}",
// "if (1>0){ext()}",
// "if (1<0){} else {ext()}",
// "if (ext>0){ext()} else {ext()}",
// IfStatement
"if (ext()>0){}",
"if (1>0){ext()}",
"if (1<0){} else {ext()}",
"if (ext>0){ext()} else {ext()}",
// // ImportDeclaration
// r#"import x from "import-default"; x()"#,
// r#"import x from "import-default"; x.z = 1"#,

View file

@ -326,6 +326,78 @@ 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:27]
1 │ const x = ()=>{}; (true ? ext : 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:32]
1 │ const x = ()=>{}; (false ? x : 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:30]
1 │ const x = ()=>{}; (ext ? x : ext)()
· ───
╰────
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Debugger statements are side-effects
╭─[no_side_effects_in_initialization.tsx:1:1]
1 │ debugger
· ────────
╰────
⚠ 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:13]
1 │ do {} while(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:4]
1 │ do ext(); while(true)
· ───
╰────
⚠ 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:5]
1 │ do {ext()} while(true)
· ───
╰────
⚠ 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:5]
1 │ if (ext()>0){}
· ───
╰────
⚠ 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 │ if (1>0){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:18]
1 │ if (1<0){} else {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:12]
1 │ if (ext>0){ext()} else {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:25]
1 │ if (ext>0){ext()} else {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:15]
1 │ const x = new ext()

View file

@ -5,7 +5,7 @@ use oxc_syntax::operator::BinaryOperator;
use crate::LintContext;
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Value {
Boolean(bool),
Number(f64),
@ -14,7 +14,7 @@ pub enum Value {
}
// We only care if it is falsy value (empty string).
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum StringValue {
Empty,
NonEmpty,
@ -49,7 +49,7 @@ impl Value {
Value::Unknown => None,
Value::Boolean(boolean) => Some(!*boolean),
Value::Number(num) => Some(*num == 0.0),
Value::String(str) => Some(matches!(str, StringValue::Empty)),
Value::String(str) => Some(!matches!(str, StringValue::Empty)),
}
}
}
@ -89,6 +89,7 @@ pub fn has_pure_notation(span: Span, ctx: &LintContext) -> bool {
}
/// Port from <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/463fa1f0bef7caa2b231a38b9c3557051f506c92/src/rules/no-side-effects-in-initialization.ts#L136-L161>
/// <https://tc39.es/ecma262/#sec-evaluatestringornumericbinaryexpression>
pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value) -> Value {
match op {
BinaryOperator::Addition => match (left, right) {
@ -106,6 +107,49 @@ pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value)
(Value::Number(a), Value::Number(b)) => Value::Number(a - b),
_ => Value::Unknown,
},
// <https://tc39.es/ecma262/#sec-islessthan>
#[allow(clippy::single_match)]
BinaryOperator::LessThan => match (left, right) {
// <https://tc39.es/ecma262/#sec-numeric-types-number-lessThan>
(Value::Unknown, Value::Number(_)) | (Value::Number(_), Value::Unknown) => {
Value::Boolean(false)
}
(Value::Number(a), Value::Number(b)) => Value::Boolean(a < b),
_ => Value::Unknown,
},
_ => Value::Unknown,
}
}
#[test]
fn test_calculate_binary_operation() {
use oxc_syntax::operator::BinaryOperator;
let fun = calculate_binary_operation;
// "+"
let op = BinaryOperator::Addition;
assert_eq!(fun(op, Value::Number(1.0), Value::Number(2.0),), Value::Number(3.0));
assert_eq!(
fun(op, Value::String(StringValue::Empty), Value::String(StringValue::Empty)),
Value::String(StringValue::Empty)
);
assert_eq!(
fun(op, Value::String(StringValue::Empty), Value::String(StringValue::NonEmpty)),
Value::String(StringValue::NonEmpty)
);
assert_eq!(
fun(op, Value::String(StringValue::NonEmpty), Value::String(StringValue::NonEmpty)),
Value::String(StringValue::NonEmpty)
);
// "-"
let op = BinaryOperator::Subtraction;
assert_eq!(fun(op, Value::Number(1.0), Value::Number(2.0),), Value::Number(-1.0));
// "<"
let op = BinaryOperator::LessThan;
assert_eq!(fun(op, Value::Number(1.0), Value::Number(2.0),), Value::Boolean(true));
assert_eq!(fun(op, Value::Unknown, Value::Number(2.0),), Value::Boolean(false));
}