diff --git a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs index 8077a1a1d..3b2f786ae 100644 --- a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs +++ b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/listener_map.rs @@ -25,7 +25,8 @@ use crate::{ ast_util::{get_declaration_of_variable, get_symbol_id_of_variable}, utils::{ calculate_binary_operation, calculate_logical_operation, calculate_unary_operation, - get_write_expr, has_comment_about_side_effect_check, has_pure_notation, no_effects, Value, + get_write_expr, has_comment_about_side_effect_check, has_pure_notation, is_pure_function, + no_effects, FunctionName, Value, }, LintContext, }; @@ -1053,7 +1054,6 @@ impl<'a> ListenerMap for CallExpression<'a> { self.callee.report_effects_when_called(options); options.called_with_new.set(old_value); } else { - // TODO: Not work now options.ctx.diagnostic(NoSideEffectsDiagnostic::Call(self.callee.span())); } } @@ -1131,8 +1131,7 @@ impl<'a> ListenerMap for IdentifierReference<'a> { } fn report_effects_when_called(&self, options: &NodeListenerOptions) { - // TODO: change to `isPureFunction` - if has_pure_notation(self.span, options.ctx) { + if is_pure_function(&FunctionName::Identifier(self), options.ctx) { return; } @@ -1281,10 +1280,16 @@ impl<'a> ListenerMap for StaticMemberExpression<'a> { } } - if let Expression::Identifier(ident) = node { - ident.report_effects_when_called(options); - } else { + let Expression::Identifier(ident) = node else { options.ctx.diagnostic(NoSideEffectsDiagnostic::CallMember(node.span())); + return; + }; + + if get_declaration_of_variable(ident, options.ctx) + .is_some_and(|_| !has_pure_notation(self.span, options.ctx)) + || !is_pure_function(&FunctionName::StaticMemberExpr(self), options.ctx) + { + options.ctx.diagnostic(NoSideEffectsDiagnostic::CallMember(self.span)); } } fn report_effects_when_assigned(&self, options: &NodeListenerOptions) { diff --git a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs index 89bb784bd..63e6bdd82 100644 --- a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs +++ b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization/mod.rs @@ -311,7 +311,7 @@ fn test() { r#"const x = ext["y"]"#, "let x = ()=>{}; x.y = 1", // MemberExpression when called - // "const x = Object.keys({})", + "const x = Object.keys({})", // MemberExpression when mutated "const x = {};x.y = ext", "const x = {y: 1};delete x.y", diff --git a/crates/oxc_linter/src/snapshots/no_side_effects_in_initialization.snap b/crates/oxc_linter/src/snapshots/no_side_effects_in_initialization.snap index e0103da98..34fba0cef 100644 --- a/crates/oxc_linter/src/snapshots/no_side_effects_in_initialization.snap +++ b/crates/oxc_linter/src/snapshots/no_side_effects_in_initialization.snap @@ -806,10 +806,10 @@ expression: no_side_effects_in_initialization · ─ ╰──── - ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling imported function - ╭─[no_side_effects_in_initialization.tsx:1:13] + ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function + ╭─[no_side_effects_in_initialization.tsx:1:30] 1 │ import * as y from "import"; y.x() - · ─ + · ─── ╰──── ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of mutating imported variable @@ -944,16 +944,16 @@ expression: no_side_effects_in_initialization · ─── ╰──── - ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext` + ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function ╭─[no_side_effects_in_initialization.tsx:1:1] 1 │ ext.x() - · ─── + · ───── ╰──── - ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling - ╭─[no_side_effects_in_initialization.tsx:1:11] + ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function + ╭─[no_side_effects_in_initialization.tsx:1:15] 1 │ const x = {}; x.y() - · ── + · ─── ╰──── ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function @@ -962,10 +962,10 @@ expression: no_side_effects_in_initialization · ─── ╰──── - ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling - ╭─[no_side_effects_in_initialization.tsx:1:16] + ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function + ╭─[no_side_effects_in_initialization.tsx:1:30] 1 │ const Object = {}; const x = Object.keys({}) - · ── + · ─────────── ╰──── ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `ext` diff --git a/crates/oxc_linter/src/utils/tree_shaking.rs b/crates/oxc_linter/src/utils/tree_shaking.rs index e2dbbae3b..73068ac06 100644 --- a/crates/oxc_linter/src/utils/tree_shaking.rs +++ b/crates/oxc_linter/src/utils/tree_shaking.rs @@ -1,10 +1,17 @@ -use oxc_ast::{ast::Expression, AstKind, CommentKind}; +use lazy_static::lazy_static; +use oxc_ast::{ + ast::{Expression, IdentifierReference, StaticMemberExpression}, + AstKind, CommentKind, +}; use oxc_semantic::AstNodeId; -use oxc_span::Span; +use oxc_span::{CompactStr, GetSpan, Span}; use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; +use rustc_hash::FxHashSet; use crate::LintContext; +mod pure_functions; + #[derive(Copy, Clone, Debug, PartialEq)] pub enum Value { Boolean(bool), @@ -85,6 +92,63 @@ pub fn get_write_expr<'a, 'b>( pub fn no_effects() {} +lazy_static! { + static ref PURE_FUNCTIONS_SET: FxHashSet<&'static str> = { + let mut set = FxHashSet::default(); + set.extend(pure_functions::PURE_FUNCTIONS); + + set + }; +} + +pub enum FunctionName<'a> { + Identifier(&'a IdentifierReference<'a>), + StaticMemberExpr(&'a StaticMemberExpression<'a>), +} + +impl<'a> FunctionName<'a> { + fn from_expression(expr: &'a Expression<'a>) -> Option { + match expr { + Expression::Identifier(ident) => Some(FunctionName::Identifier(ident)), + Expression::StaticMemberExpression(member_expr) => { + Some(FunctionName::StaticMemberExpr(member_expr)) + } + _ => None, + } + } +} + +impl GetSpan for FunctionName<'_> { + fn span(&self) -> Span { + match self { + FunctionName::Identifier(ident) => ident.span, + FunctionName::StaticMemberExpr(member_expr) => member_expr.span, + } + } +} + +pub fn is_pure_function(function_name: &FunctionName, ctx: &LintContext) -> bool { + if has_pure_notation(function_name.span(), ctx) { + return true; + } + let name = flatten_member_expr_if_possible(function_name); + PURE_FUNCTIONS_SET.contains(name.as_str()) +} + +fn flatten_member_expr_if_possible(function_name: &FunctionName) -> CompactStr { + match function_name { + FunctionName::StaticMemberExpr(static_member_expr) => { + let Some(parent_name) = FunctionName::from_expression(&static_member_expr.object) + else { + return CompactStr::from(""); + }; + let flattened_parent = flatten_member_expr_if_possible(&parent_name); + CompactStr::from(format!("{}.{}", flattened_parent, static_member_expr.property.name)) + } + FunctionName::Identifier(ident) => ident.name.to_compact_str(), + } +} + /// Comments containing @__PURE__ or #__PURE__ mark a specific function call /// or constructor invocation as side effect free. /// diff --git a/crates/oxc_linter/src/utils/tree_shaking/pure_functions.rs b/crates/oxc_linter/src/utils/tree_shaking/pure_functions.rs new file mode 100644 index 000000000..f8280da14 --- /dev/null +++ b/crates/oxc_linter/src/utils/tree_shaking/pure_functions.rs @@ -0,0 +1,370 @@ +pub const PURE_FUNCTIONS: &[&str] = &[ + "Array.isArray", + "Error", + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "unescape", + "Object", + "Object.create", + "Object.getNotifier", + "Object.getOwn", + "Object.getOwnPropertyDescriptor", + "Object.getOwnPropertyNames", + "Object.getOwnPropertySymbols", + "Object.getPrototypeOf", + "Object.is", + "Object.isExtensible", + "Object.isFrozen", + "Object.isSealed", + "Object.keys", + "Function", + "Boolean", + "Number", + "Number.isFinite", + "Number.isInteger", + "Number.isNaN", + "Number.isSafeInteger", + "Number.parseFloat", + "Number.parseInt", + "Symbol", + "Symbol.for", + "Symbol.keyFor", + "Math.abs", + "Math.acos", + "Math.acosh", + "Math.asin", + "Math.asinh", + "Math.atan", + "Math.atan2", + "Math.atanh", + "Math.cbrt", + "Math.ceil", + "Math.clz32", + "Math.cos", + "Math.cosh", + "Math.exp", + "Math.expm1", + "Math.floor", + "Math.fround", + "Math.hypot", + "Math.imul", + "Math.log", + "Math.log10", + "Math.log1p", + "Math.log2", + "Math.max", + "Math.min", + "Math.pow", + "Math.random", + "Math.round", + "Math.sign", + "Math.sin", + "Math.sinh", + "Math.sqrt", + "Math.tan", + "Math.tanh", + "Math.trunc", + "Date", + "Date.UTC", + "Date.now", + "Date.parse", + "String", + "String.fromCharCode", + "String.fromCodePoint", + "String.raw", + "RegExp", + "Map", + "Set", + "WeakMap", + "WeakSet", + "ArrayBuffer", + "ArrayBuffer.isView", + "DataView", + "JSON.parse", + "JSON.stringify", + "Promise", + "Promise.all", + "Promise.race", + "Promise.reject", + "Promise.resolve", + "Intl.Collator", + "Intl.Collator.supportedLocalesOf", + "Intl.DateTimeFormat", + "Intl.DateTimeFormat.supportedLocalesOf", + "Intl.NumberFormat", + "Intl.NumberFormat.supportedLocalesOf", + "Array", + "Int8Array", + "Uint8Array", + "Uint8ClampedArray", + "Int16Array", + "Uint16Array", + "Int32Array", + "Uint32Array", + "Float32Array", + "Float64Array", + "Array.from", + "Int8Array.from", + "Uint8Array.from", + "Uint8ClampedArray.from", + "Int16Array.from", + "Uint16Array.from", + "Int32Array.from", + "Uint32Array.from", + "Float32Array.from", + "Float64Array.from", + "Array.of", + "Int8Array.of", + "Uint8Array.of", + "Uint8ClampedArray.of", + "Int16Array.of", + "Uint16Array.of", + "Int32Array.of", + "Uint32Array.of", + "Float32Array.of", + "Float64Array.of", + "SIMD.Int8x16", + "SIMD.Int16x8", + "SIMD.Int32x4", + "SIMD.Float32x4", + "SIMD.Float64x2", + "SIMD.Int8x16.abs", + "SIMD.Int8x16.add", + "SIMD.Int8x16.and", + "SIMD.Int8x16.bool", + "SIMD.Int8x16.check", + "SIMD.Int8x16.div", + "SIMD.Int8x16.equal", + "SIMD.Int8x16.extractLane", + "SIMD.Int8x16.fromFloat32x4", + "SIMD.Int8x16.fromFloat32x4Bits", + "SIMD.Int8x16.fromFloat64x2", + "SIMD.Int8x16.fromFloat64x2Bits", + "SIMD.Int8x16.fromInt16x8Bits", + "SIMD.Int8x16.fromInt32x4", + "SIMD.Int8x16.fromInt32x4Bits", + "SIMD.Int8x16.fromInt8x16Bits", + "SIMD.Int8x16.greaterThan", + "SIMD.Int8x16.greaterThanOrEqual", + "SIMD.Int8x16.lessThan", + "SIMD.Int8x16.lessThanOrEqual", + "SIMD.Int8x16.load", + "SIMD.Int8x16.max", + "SIMD.Int8x16.maxNum", + "SIMD.Int8x16.min", + "SIMD.Int8x16.minNum", + "SIMD.Int8x16.mul", + "SIMD.Int8x16.neg", + "SIMD.Int8x16.not", + "SIMD.Int8x16.notEqual", + "SIMD.Int8x16.or", + "SIMD.Int8x16.reciprocalApproximation", + "SIMD.Int8x16.reciprocalSqrtApproximation", + "SIMD.Int8x16.replaceLane", + "SIMD.Int8x16.select", + "SIMD.Int8x16.selectBits", + "SIMD.Int8x16.shiftLeftByScalar", + "SIMD.Int8x16.shiftRightArithmeticByScalar", + "SIMD.Int8x16.shiftRightLogicalByScalar", + "SIMD.Int8x16.shuffle", + "SIMD.Int8x16.splat", + "SIMD.Int8x16.sqrt", + "SIMD.Int8x16.store", + "SIMD.Int8x16.sub", + "SIMD.Int8x16.swizzle", + "SIMD.Int8x16.xor", + "SIMD.Int16x8.abs", + "SIMD.Int16x8.add", + "SIMD.Int16x8.and", + "SIMD.Int16x8.bool", + "SIMD.Int16x8.check", + "SIMD.Int16x8.div", + "SIMD.Int16x8.equal", + "SIMD.Int16x8.extractLane", + "SIMD.Int16x8.fromFloat32x4", + "SIMD.Int16x8.fromFloat32x4Bits", + "SIMD.Int16x8.fromFloat64x2", + "SIMD.Int16x8.fromFloat64x2Bits", + "SIMD.Int16x8.fromInt16x8Bits", + "SIMD.Int16x8.fromInt32x4", + "SIMD.Int16x8.fromInt32x4Bits", + "SIMD.Int16x8.fromInt8x16Bits", + "SIMD.Int16x8.greaterThan", + "SIMD.Int16x8.greaterThanOrEqual", + "SIMD.Int16x8.lessThan", + "SIMD.Int16x8.lessThanOrEqual", + "SIMD.Int16x8.load", + "SIMD.Int16x8.max", + "SIMD.Int16x8.maxNum", + "SIMD.Int16x8.min", + "SIMD.Int16x8.minNum", + "SIMD.Int16x8.mul", + "SIMD.Int16x8.neg", + "SIMD.Int16x8.not", + "SIMD.Int16x8.notEqual", + "SIMD.Int16x8.or", + "SIMD.Int16x8.reciprocalApproximation", + "SIMD.Int16x8.reciprocalSqrtApproximation", + "SIMD.Int16x8.replaceLane", + "SIMD.Int16x8.select", + "SIMD.Int16x8.selectBits", + "SIMD.Int16x8.shiftLeftByScalar", + "SIMD.Int16x8.shiftRightArithmeticByScalar", + "SIMD.Int16x8.shiftRightLogicalByScalar", + "SIMD.Int16x8.shuffle", + "SIMD.Int16x8.splat", + "SIMD.Int16x8.sqrt", + "SIMD.Int16x8.store", + "SIMD.Int16x8.sub", + "SIMD.Int16x8.swizzle", + "SIMD.Int16x8.xor", + "SIMD.Int32x4.abs", + "SIMD.Int32x4.add", + "SIMD.Int32x4.and", + "SIMD.Int32x4.bool", + "SIMD.Int32x4.check", + "SIMD.Int32x4.div", + "SIMD.Int32x4.equal", + "SIMD.Int32x4.extractLane", + "SIMD.Int32x4.fromFloat32x4", + "SIMD.Int32x4.fromFloat32x4Bits", + "SIMD.Int32x4.fromFloat64x2", + "SIMD.Int32x4.fromFloat64x2Bits", + "SIMD.Int32x4.fromInt16x8Bits", + "SIMD.Int32x4.fromInt32x4", + "SIMD.Int32x4.fromInt32x4Bits", + "SIMD.Int32x4.fromInt8x16Bits", + "SIMD.Int32x4.greaterThan", + "SIMD.Int32x4.greaterThanOrEqual", + "SIMD.Int32x4.lessThan", + "SIMD.Int32x4.lessThanOrEqual", + "SIMD.Int32x4.load", + "SIMD.Int32x4.max", + "SIMD.Int32x4.maxNum", + "SIMD.Int32x4.min", + "SIMD.Int32x4.minNum", + "SIMD.Int32x4.mul", + "SIMD.Int32x4.neg", + "SIMD.Int32x4.not", + "SIMD.Int32x4.notEqual", + "SIMD.Int32x4.or", + "SIMD.Int32x4.reciprocalApproximation", + "SIMD.Int32x4.reciprocalSqrtApproximation", + "SIMD.Int32x4.replaceLane", + "SIMD.Int32x4.select", + "SIMD.Int32x4.selectBits", + "SIMD.Int32x4.shiftLeftByScalar", + "SIMD.Int32x4.shiftRightArithmeticByScalar", + "SIMD.Int32x4.shiftRightLogicalByScalar", + "SIMD.Int32x4.shuffle", + "SIMD.Int32x4.splat", + "SIMD.Int32x4.sqrt", + "SIMD.Int32x4.store", + "SIMD.Int32x4.sub", + "SIMD.Int32x4.swizzle", + "SIMD.Int32x4.xor", + "SIMD.Float32x4.abs", + "SIMD.Float32x4.add", + "SIMD.Float32x4.and", + "SIMD.Float32x4.bool", + "SIMD.Float32x4.check", + "SIMD.Float32x4.div", + "SIMD.Float32x4.equal", + "SIMD.Float32x4.extractLane", + "SIMD.Float32x4.fromFloat32x4", + "SIMD.Float32x4.fromFloat32x4Bits", + "SIMD.Float32x4.fromFloat64x2", + "SIMD.Float32x4.fromFloat64x2Bits", + "SIMD.Float32x4.fromInt16x8Bits", + "SIMD.Float32x4.fromInt32x4", + "SIMD.Float32x4.fromInt32x4Bits", + "SIMD.Float32x4.fromInt8x16Bits", + "SIMD.Float32x4.greaterThan", + "SIMD.Float32x4.greaterThanOrEqual", + "SIMD.Float32x4.lessThan", + "SIMD.Float32x4.lessThanOrEqual", + "SIMD.Float32x4.load", + "SIMD.Float32x4.max", + "SIMD.Float32x4.maxNum", + "SIMD.Float32x4.min", + "SIMD.Float32x4.minNum", + "SIMD.Float32x4.mul", + "SIMD.Float32x4.neg", + "SIMD.Float32x4.not", + "SIMD.Float32x4.notEqual", + "SIMD.Float32x4.or", + "SIMD.Float32x4.reciprocalApproximation", + "SIMD.Float32x4.reciprocalSqrtApproximation", + "SIMD.Float32x4.replaceLane", + "SIMD.Float32x4.select", + "SIMD.Float32x4.selectBits", + "SIMD.Float32x4.shiftLeftByScalar", + "SIMD.Float32x4.shiftRightArithmeticByScalar", + "SIMD.Float32x4.shiftRightLogicalByScalar", + "SIMD.Float32x4.shuffle", + "SIMD.Float32x4.splat", + "SIMD.Float32x4.sqrt", + "SIMD.Float32x4.store", + "SIMD.Float32x4.sub", + "SIMD.Float32x4.swizzle", + "SIMD.Float32x4.xor", + "SIMD.Float64x2.abs", + "SIMD.Float64x2.add", + "SIMD.Float64x2.and", + "SIMD.Float64x2.bool", + "SIMD.Float64x2.check", + "SIMD.Float64x2.div", + "SIMD.Float64x2.equal", + "SIMD.Float64x2.extractLane", + "SIMD.Float64x2.fromFloat32x4", + "SIMD.Float64x2.fromFloat32x4Bits", + "SIMD.Float64x2.fromFloat64x2", + "SIMD.Float64x2.fromFloat64x2Bits", + "SIMD.Float64x2.fromInt16x8Bits", + "SIMD.Float64x2.fromInt32x4", + "SIMD.Float64x2.fromInt32x4Bits", + "SIMD.Float64x2.fromInt8x16Bits", + "SIMD.Float64x2.greaterThan", + "SIMD.Float64x2.greaterThanOrEqual", + "SIMD.Float64x2.lessThan", + "SIMD.Float64x2.lessThanOrEqual", + "SIMD.Float64x2.load", + "SIMD.Float64x2.max", + "SIMD.Float64x2.maxNum", + "SIMD.Float64x2.min", + "SIMD.Float64x2.minNum", + "SIMD.Float64x2.mul", + "SIMD.Float64x2.neg", + "SIMD.Float64x2.not", + "SIMD.Float64x2.notEqual", + "SIMD.Float64x2.or", + "SIMD.Float64x2.reciprocalApproximation", + "SIMD.Float64x2.reciprocalSqrtApproximation", + "SIMD.Float64x2.replaceLane", + "SIMD.Float64x2.select", + "SIMD.Float64x2.selectBits", + "SIMD.Float64x2.shiftLeftByScalar", + "SIMD.Float64x2.shiftRightArithmeticByScalar", + "SIMD.Float64x2.shiftRightLogicalByScalar", + "SIMD.Float64x2.shuffle", + "SIMD.Float64x2.splat", + "SIMD.Float64x2.sqrt", + "SIMD.Float64x2.store", + "SIMD.Float64x2.sub", + "SIMD.Float64x2.swizzle", + "SIMD.Float64x2.xor", +];