feat(linter/tree-shaking): support ExportNamedDeclaration (#3072)

This commit is contained in:
Wang Wenzhe 2024-04-23 02:19:34 +08:00 committed by GitHub
parent 942b2ba084
commit c3ec710b3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 136 additions and 34 deletions

View file

@ -3,12 +3,13 @@ use std::cell::{Cell, RefCell};
use oxc_ast::{
ast::{
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentTarget,
BinaryExpression, BindingPattern, BindingPatternKind, CallExpression, Class, ClassBody,
ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration,
ExportDefaultDeclarationKind, Expression, FormalParameter, Function, IdentifierReference,
MemberExpression, ModuleDeclaration, NewExpression, ParenthesizedExpression,
PrivateFieldExpression, Program, PropertyKey, SimpleAssignmentTarget, Statement,
StaticMemberExpression, ThisExpression, VariableDeclarator,
BinaryExpression, BindingIdentifier, BindingPattern, BindingPatternKind, CallExpression,
Class, ClassBody, ClassElement, ComputedMemberExpression, ConditionalExpression,
Declaration, ExportDefaultDeclarationKind, ExportSpecifier, Expression, FormalParameter,
Function, IdentifierReference, MemberExpression, ModuleDeclaration, ModuleExportName,
NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey,
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
VariableDeclarator,
},
AstKind,
};
@ -19,8 +20,8 @@ use rustc_hash::FxHashSet;
use crate::{
ast_util::{get_declaration_of_variable, get_symbol_id_of_variable},
utils::{
calculate_binary_operation, get_leading_tree_shaking_comment, get_write_expr,
has_pure_notation, no_effects, Value, COMMENT_NO_SIDE_EFFECT_WHEN_CALLED,
calculate_binary_operation, get_write_expr, has_comment_about_side_effect_check,
has_pure_notation, no_effects, Value,
},
LintContext,
};
@ -88,18 +89,23 @@ impl<'a> ListenerMap for Statement<'a> {
| ModuleDeclaration::ImportDeclaration(_) => {
no_effects();
}
ModuleDeclaration::ExportDefaultDeclaration(b) => {
if let ExportDefaultDeclarationKind::Expression(expr) = &b.declaration {
if let Some(comment) =
get_leading_tree_shaking_comment(expr.span(), options.ctx)
{
if comment.contains(COMMENT_NO_SIDE_EFFECT_WHEN_CALLED) {
expr.report_effects_when_called(options);
}
ModuleDeclaration::ExportDefaultDeclaration(stmt) => {
if let ExportDefaultDeclarationKind::Expression(expr) = &stmt.declaration {
if has_comment_about_side_effect_check(expr.span(), options.ctx) {
expr.report_effects_when_called(options);
}
expr.report_effects(options);
}
}
ModuleDeclaration::ExportNamedDeclaration(stmt) => {
stmt.specifiers.iter().for_each(|specifier| {
specifier.report_effects(options);
});
if let Some(decl) = &stmt.declaration {
decl.report_effects(options);
}
}
_ => {}
},
Self::TryStatement(stmt) => {
@ -151,12 +157,39 @@ impl<'a> ListenerMap for Statement<'a> {
}
}
impl<'a> ListenerMap for ExportSpecifier<'a> {
fn report_effects(&self, options: &NodeListenerOptions) {
let ctx = options.ctx;
let symbol_table = ctx.symbols();
if has_comment_about_side_effect_check(self.exported.span(), ctx) {
let ModuleExportName::Identifier(ident_name) = &self.exported else {
return;
};
let Some(symbol_id) = options.ctx.symbols().get_symbol_id_from_name(&ident_name.name)
else {
return;
};
for reference in symbol_table.get_resolved_references(symbol_id) {
if reference.is_write() {
let node_id = reference.node_id();
if let Some(expr) = get_write_expr(node_id, ctx) {
expr.report_effects_when_called(options);
}
}
}
let symbol_table = ctx.semantic().symbols();
let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id));
node.report_effects_when_called(options);
}
}
}
// we don't need implement all AstNode
// it's same as `reportSideEffectsInDefinitionWhenCalled` in eslint-plugin-tree-shaking
// <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/463fa1f0bef7caa2b231a38b9c3557051f506c92/src/rules/no-side-effects-in-initialization.ts#L1070-L1080>
impl<'a> ListenerMap for AstNode<'a> {
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
#[allow(clippy::single_match)]
match self.kind() {
AstKind::VariableDeclarator(decl) => {
if let Some(init) = &decl.init {
@ -217,6 +250,13 @@ impl<'a> ListenerMap for Declaration<'a> {
Self::ClassDeclaration(decl) => {
decl.report_effects(options);
}
Self::FunctionDeclaration(function) => {
if let Some(id) = &function.id {
if has_comment_about_side_effect_check(id.span, options.ctx) {
id.report_effects_when_called(options);
}
}
}
_ => {}
}
}
@ -305,6 +345,9 @@ impl<'a> ListenerMap for PropertyKey<'a> {
impl<'a> ListenerMap for VariableDeclarator<'a> {
fn report_effects(&self, options: &NodeListenerOptions) {
self.id.report_effects(options);
if has_comment_about_side_effect_check(self.id.span(), options.ctx) {
self.id.report_effects_when_called(options);
}
if let Some(init) = &self.init {
init.report_effects(options);
@ -334,6 +377,34 @@ impl<'a> ListenerMap for BindingPattern<'a> {
}
}
}
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
if let BindingPatternKind::BindingIdentifier(ident) = &self.kind {
ident.report_effects_when_called(options);
}
}
}
impl<'a> ListenerMap for BindingIdentifier<'a> {
fn report_effects(&self, _options: &NodeListenerOptions) {
no_effects();
}
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
let ctx = options.ctx;
if let Some(symbol_id) = self.symbol_id.get() {
let symbol_table = ctx.semantic().symbols();
for reference in symbol_table.get_resolved_references(symbol_id) {
if reference.is_write() {
let node_id = reference.node_id();
if let Some(expr) = get_write_expr(node_id, ctx) {
expr.report_effects_when_called(options);
}
}
}
let symbol_table = ctx.semantic().symbols();
let node = ctx.nodes().get_node(symbol_table.get_declaration(symbol_id));
node.report_effects_when_called(options);
}
}
}
impl<'a> ListenerMap for Expression<'a> {

View file

@ -190,16 +190,16 @@ fn test() {
"export default (function(){})",
"const x = function(){}; export default /* tree-shaking no-side-effects-when-called */ x",
"export default /* tree-shaking no-side-effects-when-called */ function(){}",
// // ExportNamedDeclaration
// "export const x = ext",
// "export function x(){ext()}",
// "const x = ext; export {x}",
// r#"export {x} from "import""#,
// r#"export {x as y} from "import""#,
// r#"export {x as default} from "import""#,
// "export const /* tree-shaking no-side-effects-when-called */ x = function(){}",
// "export function /* tree-shaking no-side-effects-when-called */ x(){}",
// "const x = function(){}; export {/* tree-shaking no-side-effects-when-called */ x}",
// ExportNamedDeclaration
"export const x = ext",
"export function x(){ext()}",
"const x = ext; export {x}",
r#"export {x} from "import""#,
r#"export {x as y} from "import""#,
r#"export {x as default} from "import""#,
"export const /* tree-shaking no-side-effects-when-called */ x = function(){}",
"export function /* tree-shaking no-side-effects-when-called */ x(){}",
"const x = function(){}; export {/* tree-shaking no-side-effects-when-called */ x}",
// // ExpressionStatement
// "const x = 1",
// // ForInStatement
@ -460,11 +460,11 @@ fn test() {
"export default ext()",
"export default /* tree-shaking no-side-effects-when-called */ ext",
"const x = ext; export default /* tree-shaking no-side-effects-when-called */ x",
// // ExportNamedDeclaration
// "export const x = ext()",
// "export const /* tree-shaking no-side-effects-when-called */ x = ext",
// "export function /* tree-shaking no-side-effects-when-called */ x(){ext()}",
// "const x = ext; export {/* tree-shaking no-side-effects-when-called */ x}",
// ExportNamedDeclaration
"export const x = ext()",
"export const /* tree-shaking no-side-effects-when-called */ x = ext",
"export function /* tree-shaking no-side-effects-when-called */ x(){ext()}",
"const x = ext; export {/* tree-shaking no-side-effects-when-called */ x}",
// // ExpressionStatement
// "ext()",
// // ForInStatement

View file

@ -386,6 +386,30 @@ 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:18]
1 │ export 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:65]
1 │ export const /* tree-shaking no-side-effects-when-called */ 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:68]
1 │ export function /* tree-shaking no-side-effects-when-called */ 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:11]
1 │ const x = ext; export {/* tree-shaking no-side-effects-when-called */ 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:5]
1 │ if (ext()>0){}

View file

@ -89,16 +89,23 @@ pub fn has_pure_notation(span: Span, ctx: &LintContext) -> bool {
}
const TREE_SHAKING_COMMENT_ID: &str = "tree-shaking";
pub const COMMENT_NO_SIDE_EFFECT_WHEN_CALLED: &str = "no-side-effects-when-called";
const COMMENT_NO_SIDE_EFFECT_WHEN_CALLED: &str = "no-side-effects-when-called";
fn is_tree_shaking_comment(comment: &str) -> bool {
comment.trim_start().starts_with(TREE_SHAKING_COMMENT_ID)
}
/// check if the `span` has a leading comment for opening side effect check.
/// e.g. `export default /* tree-shaking no-side-effects-when-called */ ext`
pub fn has_comment_about_side_effect_check(span: Span, ctx: &LintContext) -> bool {
get_leading_tree_shaking_comment(span, ctx)
.is_some_and(|comment| comment.contains(COMMENT_NO_SIDE_EFFECT_WHEN_CALLED))
}
/// Get the nearest comment before the `span`, return `None` if no leading comment is founded.
///
/// # Examples
/// ```
/// ```javascript
/// /* valid comment for `a` */ let a = 1;
///
/// // valid comment for `b`