feat(linter/tree-shaking): support JSX (#3139)

This commit is contained in:
Wang Wenzhe 2024-04-30 11:25:39 +08:00 committed by GitHub
parent 16a31e95b8
commit 733361822e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 309 additions and 40 deletions

View file

@ -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);

View file

@ -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 = <X test="3"/>"#,
// "class X {}; const x = <X test={3}/>",
// "class X {}; const x = <X test=<X/>/>",
// // JSXElement
// "class X {}; const x = <X/>",
// "class X {}; const x = <X>Text</X>",
// // JSXEmptyExpression
// "class X {}; const x = <X>{}</X>",
// // JSXExpressionContainer
// "class X {}; const x = <X>{3}</X>",
// // JSXIdentifier
// "class X {}; const x = <X/>",
// "const X = class {constructor() {this.x = 1}}; const x = <X/>",
// // JSXOpeningElement
// "class X {}; const x = <X/>",
// "class X {}; const x = <X></X>",
// r#"class X {}; const x = <X test="3"/>"#,
// // JSXSpreadAttribute
// "class X {}; const x = <X {...{x: 3}}/>",
// JSXAttribute
r#"class X {}; const x = <X test="3"/>"#,
"class X {}; const x = <X test={3}/>",
"class X {}; const x = <X test=<X/>/>",
// JSXElement
"class X {}; const x = <X/>",
"class X {}; const x = <X>Text</X>",
// JSXEmptyExpression
"class X {}; const x = <X>{}</X>",
// JSXExpressionContainer
"class X {}; const x = <X>{3}</X>",
// JSXIdentifier
"class X {}; const x = <X/>",
"const X = class {constructor() {this.x = 1}}; const x = <X/>",
// JSXOpeningElement
"class X {}; const x = <X/>",
"class X {}; const x = <X></X>",
r#"class X {}; const x = <X test="3"/>"#,
// JSXSpreadAttribute
"class X {}; const x = <X {...{x: 3}}/>",
// // 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 = <X test={ext()}/>",
// "class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>",
// // JSXElement
// "class X {constructor(){ext()}}; const x = <X/>",
// "class X {}; const x = <X>{ext()}</X>",
// // JSXExpressionContainer
// "class X {}; const x = <X>{ext()}</X>",
// // JSXIdentifier
// "class X {constructor(){ext()}}; const x = <X/>",
// "const X = class {constructor(){ext()}}; const x = <X/>",
// "const x = <Ext/>",
// // JSXMemberExpression
// "const X = {Y: ext}; const x = <X.Y />",
// // JSXOpeningElement
// "class X {}; const x = <X test={ext()}/>",
// // JSXSpreadAttribute
// "class X {}; const x = <X {...{x: ext()}}/>",
// JSXAttribute
"class X {}; const x = <X test={ext()}/>",
"class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>",
// JSXElement
"class X {constructor(){ext()}}; const x = <X/>",
"class X {}; const x = <X>{ext()}</X>",
// JSXExpressionContainer
"class X {}; const x = <X>{ext()}</X>",
// JSXIdentifier
"class X {constructor(){ext()}}; const x = <X/>",
"const X = class {constructor(){ext()}}; const x = <X/>",
"const x = <Ext/>",
// JSXMemberExpression
"const X = {Y: ext}; const x = <X.Y />",
// JSXOpeningElement
"class X {}; const x = <X test={ext()}/>",
// JSXSpreadAttribute
"class X {}; const x = <X {...{x: ext()}}/>",
// // LabeledStatement
// "loop: for(;true;){ext()}",
// // Literal

View file

@ -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 = <X test={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:36]
1 │ class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>
· ───
╰────
⚠ 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 = <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 = <X>{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:27]
1 │ class X {}; const x = <X>{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:24]
1 │ class X {constructor(){ext()}}; const x = <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 = <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 = <Ext/>
· ───
╰────
⚠ 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 = <X.Y />
· ─
╰────
⚠ 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 = <X test={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:34]
1 │ class X {}; const x = <X {...{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:15]
1 │ const x = new ext()