mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/tree-shaking): support JSX (#3139)
This commit is contained in:
parent
16a31e95b8
commit
733361822e
3 changed files with 309 additions and 40 deletions
|
|
@ -7,8 +7,11 @@ use oxc_ast::{
|
||||||
AssignmentTarget, BinaryExpression, BindingIdentifier, BindingPattern, BindingPatternKind,
|
AssignmentTarget, BinaryExpression, BindingIdentifier, BindingPattern, BindingPatternKind,
|
||||||
CallExpression, Class, ClassBody, ClassElement, ComputedMemberExpression,
|
CallExpression, Class, ClassBody, ClassElement, ComputedMemberExpression,
|
||||||
ConditionalExpression, Declaration, ExportSpecifier, Expression, ForStatementInit,
|
ConditionalExpression, Declaration, ExportSpecifier, Expression, ForStatementInit,
|
||||||
FormalParameter, Function, IdentifierReference, MemberExpression, ModuleExportName,
|
FormalParameter, Function, IdentifierReference, JSXAttribute, JSXAttributeItem,
|
||||||
NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey,
|
JSXAttributeValue, JSXChild, JSXElement, JSXElementName, JSXExpression,
|
||||||
|
JSXExpressionContainer, JSXFragment, JSXIdentifier, JSXOpeningElement, MemberExpression,
|
||||||
|
ModuleExportName, NewExpression, ObjectExpression, ObjectPropertyKind,
|
||||||
|
ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey,
|
||||||
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
|
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
},
|
},
|
||||||
|
|
@ -347,7 +350,10 @@ impl<'a> ListenerMap for ClassBody<'a> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(constructor) = constructor {
|
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);
|
constructor.report_effects_when_called(options);
|
||||||
|
options.has_valid_this.set(old_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.body
|
self.body
|
||||||
|
|
@ -491,6 +497,12 @@ impl<'a> ListenerMap for Expression<'a> {
|
||||||
Self::ConditionalExpression(expr) => {
|
Self::ConditionalExpression(expr) => {
|
||||||
expr.get_value_and_report_effects(options);
|
expr.get_value_and_report_effects(options);
|
||||||
}
|
}
|
||||||
|
Self::JSXElement(expr) => {
|
||||||
|
expr.report_effects(options);
|
||||||
|
}
|
||||||
|
Self::ObjectExpression(expr) => {
|
||||||
|
expr.report_effects(options);
|
||||||
|
}
|
||||||
Self::ArrowFunctionExpression(_)
|
Self::ArrowFunctionExpression(_)
|
||||||
| Self::FunctionExpression(_)
|
| Self::FunctionExpression(_)
|
||||||
| Self::Identifier(_)
|
| 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> {
|
impl<'a> ListenerMap for ConditionalExpression<'a> {
|
||||||
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
|
fn get_value_and_report_effects(&self, options: &NodeListenerOptions) -> Value {
|
||||||
let test_result = self.test.get_value_and_report_effects(options);
|
let test_result = self.test.get_value_and_report_effects(options);
|
||||||
|
|
|
||||||
|
|
@ -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 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()"#,
|
||||||
r#"import {x} from "import"; /* @__PURE__ */ x()"#,
|
r#"import {x} from "import"; /* @__PURE__ */ x()"#,
|
||||||
// // JSXAttribute
|
// JSXAttribute
|
||||||
// r#"class X {}; const x = <X test="3"/>"#,
|
r#"class X {}; const x = <X test="3"/>"#,
|
||||||
// "class X {}; const x = <X test={3}/>",
|
"class X {}; const x = <X test={3}/>",
|
||||||
// "class X {}; const x = <X test=<X/>/>",
|
"class X {}; const x = <X test=<X/>/>",
|
||||||
// // JSXElement
|
// JSXElement
|
||||||
// "class X {}; const x = <X/>",
|
"class X {}; const x = <X/>",
|
||||||
// "class X {}; const x = <X>Text</X>",
|
"class X {}; const x = <X>Text</X>",
|
||||||
// // JSXEmptyExpression
|
// JSXEmptyExpression
|
||||||
// "class X {}; const x = <X>{}</X>",
|
"class X {}; const x = <X>{}</X>",
|
||||||
// // JSXExpressionContainer
|
// JSXExpressionContainer
|
||||||
// "class X {}; const x = <X>{3}</X>",
|
"class X {}; const x = <X>{3}</X>",
|
||||||
// // JSXIdentifier
|
// JSXIdentifier
|
||||||
// "class X {}; const x = <X/>",
|
"class X {}; const x = <X/>",
|
||||||
// "const X = class {constructor() {this.x = 1}}; const x = <X/>",
|
"const X = class {constructor() {this.x = 1}}; const x = <X/>",
|
||||||
// // JSXOpeningElement
|
// JSXOpeningElement
|
||||||
// "class X {}; const x = <X/>",
|
"class X {}; const x = <X/>",
|
||||||
// "class X {}; const x = <X></X>",
|
"class X {}; const x = <X></X>",
|
||||||
// r#"class X {}; const x = <X test="3"/>"#,
|
r#"class X {}; const x = <X test="3"/>"#,
|
||||||
// // JSXSpreadAttribute
|
// JSXSpreadAttribute
|
||||||
// "class X {}; const x = <X {...{x: 3}}/>",
|
"class X {}; const x = <X {...{x: 3}}/>",
|
||||||
// // LabeledStatement
|
// // LabeledStatement
|
||||||
// "loop: for(;true;){continue loop}",
|
// "loop: for(;true;){continue loop}",
|
||||||
// // Literal
|
// // Literal
|
||||||
|
|
@ -550,24 +550,24 @@ fn test() {
|
||||||
r#"import {x as y} from "import"; y.a = 1"#,
|
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()"#,
|
||||||
r#"import * as y from "import"; y.x = 1"#,
|
r#"import * as y from "import"; y.x = 1"#,
|
||||||
// // JSXAttribute
|
// JSXAttribute
|
||||||
// "class X {}; const x = <X test={ext()}/>",
|
"class X {}; const x = <X test={ext()}/>",
|
||||||
// "class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>",
|
"class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>",
|
||||||
// // JSXElement
|
// JSXElement
|
||||||
// "class X {constructor(){ext()}}; const x = <X/>",
|
"class X {constructor(){ext()}}; const x = <X/>",
|
||||||
// "class X {}; const x = <X>{ext()}</X>",
|
"class X {}; const x = <X>{ext()}</X>",
|
||||||
// // JSXExpressionContainer
|
// JSXExpressionContainer
|
||||||
// "class X {}; const x = <X>{ext()}</X>",
|
"class X {}; const x = <X>{ext()}</X>",
|
||||||
// // JSXIdentifier
|
// JSXIdentifier
|
||||||
// "class X {constructor(){ext()}}; const x = <X/>",
|
"class X {constructor(){ext()}}; const x = <X/>",
|
||||||
// "const X = class {constructor(){ext()}}; const x = <X/>",
|
"const X = class {constructor(){ext()}}; const x = <X/>",
|
||||||
// "const x = <Ext/>",
|
"const x = <Ext/>",
|
||||||
// // JSXMemberExpression
|
// JSXMemberExpression
|
||||||
// "const X = {Y: ext}; const x = <X.Y />",
|
"const X = {Y: ext}; const x = <X.Y />",
|
||||||
// // JSXOpeningElement
|
// JSXOpeningElement
|
||||||
// "class X {}; const x = <X test={ext()}/>",
|
"class X {}; const x = <X test={ext()}/>",
|
||||||
// // JSXSpreadAttribute
|
// JSXSpreadAttribute
|
||||||
// "class X {}; const x = <X {...{x: ext()}}/>",
|
"class X {}; const x = <X {...{x: ext()}}/>",
|
||||||
// // LabeledStatement
|
// // LabeledStatement
|
||||||
// "loop: for(;true;){ext()}",
|
// "loop: for(;true;){ext()}",
|
||||||
// // Literal
|
// // Literal
|
||||||
|
|
|
||||||
|
|
@ -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`
|
⚠ 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]
|
╭─[no_side_effects_in_initialization.tsx:1:15]
|
||||||
1 │ const x = new ext()
|
1 │ const x = new ext()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue