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 57fcd85e9..8691328c2 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 @@ -7,8 +7,11 @@ use oxc_ast::{ AssignmentTarget, BinaryExpression, BindingIdentifier, BindingPattern, BindingPatternKind, CallExpression, Class, ClassBody, ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration, ExportSpecifier, Expression, ForStatementInit, - FormalParameter, Function, IdentifierReference, MemberExpression, ModuleExportName, - NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey, + FormalParameter, Function, IdentifierReference, JSXAttribute, JSXAttributeItem, + JSXAttributeValue, JSXChild, JSXElement, JSXElementName, JSXExpression, + JSXExpressionContainer, JSXFragment, JSXIdentifier, JSXOpeningElement, MemberExpression, + ModuleExportName, NewExpression, ObjectExpression, ObjectPropertyKind, + ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey, SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression, VariableDeclarator, }, @@ -347,7 +350,10 @@ impl<'a> ListenerMap for ClassBody<'a> { }); if let Some(constructor) = constructor { + let old_val = options.has_valid_this.get(); + options.has_valid_this.set(options.called_with_new.get()); constructor.report_effects_when_called(options); + options.has_valid_this.set(old_val); } self.body @@ -491,6 +497,12 @@ impl<'a> ListenerMap for Expression<'a> { Self::ConditionalExpression(expr) => { expr.get_value_and_report_effects(options); } + Self::JSXElement(expr) => { + expr.report_effects(options); + } + Self::ObjectExpression(expr) => { + expr.report_effects(options); + } Self::ArrowFunctionExpression(_) | Self::FunctionExpression(_) | Self::Identifier(_) @@ -584,6 +596,197 @@ fn defined_custom_report_effects_when_called(expr: &Expression) -> bool { ) } +impl<'a> ListenerMap for ObjectExpression<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + self.properties.iter().for_each(|property| match property { + ObjectPropertyKind::ObjectProperty(p) => { + p.key.report_effects(options); + p.value.report_effects(options); + } + ObjectPropertyKind::SpreadProperty(spreed) => { + spreed.argument.report_effects(options); + } + }); + } +} + +impl<'a> ListenerMap for JSXElement<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + self.opening_element.report_effects(options); + self.children.iter().for_each(|child| { + child.report_effects(options); + }); + } +} + +impl<'a> ListenerMap for JSXChild<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + match self { + JSXChild::Element(element) => { + element.report_effects(options); + } + JSXChild::Spread(spread) => { + spread.expression.report_effects(options); + } + JSXChild::Fragment(fragment) => { + fragment.report_effects(options); + } + JSXChild::ExpressionContainer(container) => { + container.report_effects(options); + } + JSXChild::Text(_) => { + no_effects(); + } + } + } +} + +impl<'a> ListenerMap for JSXOpeningElement<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + self.name.report_effects_when_called(options); + self.attributes.iter().for_each(|attr| attr.report_effects(options)); + } +} + +impl<'a> ListenerMap for JSXElementName<'a> { + fn report_effects_when_called(&self, options: &NodeListenerOptions) { + match self { + Self::Identifier(ident) => ident.report_effects_when_called(options), + Self::NamespacedName(name) => name.property.report_effects_when_called(options), + Self::MemberExpression(member) => { + member.property.report_effects_when_called(options); + } + } + } +} + +impl<'a> ListenerMap for JSXIdentifier<'a> { + fn report_effects_when_called(&self, options: &NodeListenerOptions) { + if self.name.chars().next().is_some_and(char::is_uppercase) { + let Some(symbol_id) = options.ctx.symbols().get_symbol_id_from_name(&self.name) else { + options.ctx.diagnostic(NoSideEffectsDiagnostic::CallGlobal( + self.name.to_compact_str(), + self.span, + )); + return; + }; + + for reference in options.ctx.symbols().get_resolved_references(symbol_id) { + if reference.is_write() { + let node_id = reference.node_id(); + if let Some(expr) = get_write_expr(node_id, options.ctx) { + let old_val = options.called_with_new.get(); + options.called_with_new.set(true); + expr.report_effects_when_called(options); + options.called_with_new.set(old_val); + } + } + } + let symbol_table = options.ctx.semantic().symbols(); + let node = options.ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)); + let old_val = options.called_with_new.get(); + options.called_with_new.set(true); + node.report_effects_when_called(options); + options.called_with_new.set(old_val); + } + } +} + +impl<'a> ListenerMap for JSXAttributeItem<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + match self { + Self::Attribute(attribute) => { + attribute.report_effects(options); + } + Self::SpreadAttribute(attribute) => { + attribute.argument.report_effects(options); + } + } + } +} + +impl<'a> ListenerMap for JSXAttribute<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + if let Some(value) = &self.value { + match value { + JSXAttributeValue::ExpressionContainer(container) => { + container.report_effects(options); + } + JSXAttributeValue::Element(element) => { + element.report_effects(options); + } + JSXAttributeValue::Fragment(fragment) => { + fragment.report_effects(options); + } + JSXAttributeValue::StringLiteral(_) => { + no_effects(); + } + } + } + } +} + +impl<'a> ListenerMap for JSXExpressionContainer<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + self.expression.report_effects(options); + } +} + +impl<'a> ListenerMap for JSXExpression<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + match self { + Self::ArrayExpression(array_expr) => { + array_expr.elements.iter().for_each(|el| el.report_effects(options)); + } + Self::AssignmentExpression(assign_expr) => { + assign_expr.left.report_effects_when_assigned(options); + assign_expr.right.report_effects(options); + } + Self::CallExpression(call_expr) => { + call_expr.report_effects(options); + } + Self::ParenthesizedExpression(expr) => { + expr.report_effects(options); + } + Self::NewExpression(expr) => { + expr.report_effects(options); + } + Self::AwaitExpression(expr) => { + expr.argument.report_effects(options); + } + Self::BinaryExpression(expr) => { + expr.get_value_and_report_effects(options); + } + Self::ClassExpression(expr) => { + expr.report_effects(options); + } + Self::ConditionalExpression(expr) => { + expr.get_value_and_report_effects(options); + } + Self::JSXElement(expr) => { + expr.report_effects(options); + } + Self::ObjectExpression(expr) => { + expr.report_effects(options); + } + Self::ArrowFunctionExpression(_) + | Self::EmptyExpression(_) + | Self::FunctionExpression(_) + | Self::Identifier(_) + | Self::MetaProperty(_) + | Self::Super(_) + | Self::ThisExpression(_) => no_effects(), + _ => {} + } + } +} + +impl<'a> ListenerMap for JSXFragment<'a> { + fn report_effects(&self, options: &NodeListenerOptions) { + self.children.iter().for_each(|child| child.report_effects(options)); + } +} + 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); 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 3651e9e67..cdc5e60da 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 @@ -260,26 +260,26 @@ fn test() { r#"import {x as /* tree-shaking no-side-effects-when-called */ y} from "import-no-effects"; y()"#, r#"import {x} from "import"; /*@__PURE__*/ x()"#, r#"import {x} from "import"; /* @__PURE__ */ x()"#, - // // JSXAttribute - // r#"class X {}; const x = "#, - // "class X {}; const x = ", - // "class X {}; const x = />", - // // JSXElement - // "class X {}; const x = ", - // "class X {}; const x = Text", - // // JSXEmptyExpression - // "class X {}; const x = {}", - // // JSXExpressionContainer - // "class X {}; const x = {3}", - // // JSXIdentifier - // "class X {}; const x = ", - // "const X = class {constructor() {this.x = 1}}; const x = ", - // // JSXOpeningElement - // "class X {}; const x = ", - // "class X {}; const x = ", - // r#"class X {}; const x = "#, - // // JSXSpreadAttribute - // "class X {}; const x = ", + // JSXAttribute + r#"class X {}; const x = "#, + "class X {}; const x = ", + "class X {}; const x = />", + // JSXElement + "class X {}; const x = ", + "class X {}; const x = Text", + // JSXEmptyExpression + "class X {}; const x = {}", + // JSXExpressionContainer + "class X {}; const x = {3}", + // JSXIdentifier + "class X {}; const x = ", + "const X = class {constructor() {this.x = 1}}; const x = ", + // JSXOpeningElement + "class X {}; const x = ", + "class X {}; const x = ", + r#"class X {}; const x = "#, + // JSXSpreadAttribute + "class X {}; const x = ", // // LabeledStatement // "loop: for(;true;){continue loop}", // // Literal @@ -550,24 +550,24 @@ fn test() { r#"import {x as y} from "import"; y.a = 1"#, r#"import * as y from "import"; y.x()"#, r#"import * as y from "import"; y.x = 1"#, - // // JSXAttribute - // "class X {}; const x = ", - // "class X {}; class Y {constructor(){ext()}}; const x = />", - // // JSXElement - // "class X {constructor(){ext()}}; const x = ", - // "class X {}; const x = {ext()}", - // // JSXExpressionContainer - // "class X {}; const x = {ext()}", - // // JSXIdentifier - // "class X {constructor(){ext()}}; const x = ", - // "const X = class {constructor(){ext()}}; const x = ", - // "const x = ", - // // JSXMemberExpression - // "const X = {Y: ext}; const x = ", - // // JSXOpeningElement - // "class X {}; const x = ", - // // JSXSpreadAttribute - // "class X {}; const x = ", + // JSXAttribute + "class X {}; const x = ", + "class X {}; class Y {constructor(){ext()}}; const x = />", + // JSXElement + "class X {constructor(){ext()}}; const x = ", + "class X {}; const x = {ext()}", + // JSXExpressionContainer + "class X {}; const x = {ext()}", + // JSXIdentifier + "class X {constructor(){ext()}}; const x = ", + "const X = class {constructor(){ext()}}; const x = ", + "const x = ", + // JSXMemberExpression + "const X = {Y: ext}; const x = ", + // JSXOpeningElement + "class X {}; const x = ", + // JSXSpreadAttribute + "class X {}; const x = ", // // LabeledStatement // "loop: for(;true;){ext()}", // // Literal 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 f55643a0f..0af1cf523 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 @@ -812,6 +812,72 @@ 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:32] + 1 │ class X {}; const 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:36] + 1 │ class X {}; class Y {constructor(){ext()}}; const 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:24] + 1 │ class X {constructor(){ext()}}; const 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:27] + 1 │ class X {}; const 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:27] + 1 │ class X {}; const 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:24] + 1 │ class X {constructor(){ext()}}; const 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 = class {constructor(){ext()}}; const 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:12] + 1 │ const x = + · ─── + ╰──── + + ⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling global function `Y` + ╭─[no_side_effects_in_initialization.tsx:1:34] + 1 │ const X = {Y: ext}; const 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 │ class X {}; const 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:34] + 1 │ class X {}; const 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:15] 1 │ const x = new ext()