feat(linter/tree-shaking): support ExportDefaultDeclaration (#3052)

This commit is contained in:
Wang Wenzhe 2024-04-22 10:00:55 +08:00 committed by GitHub
parent df7b9ee8da
commit 1a1ba11a3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 132 additions and 27 deletions

View file

@ -4,11 +4,11 @@ use oxc_ast::{
ast::{
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentTarget,
BinaryExpression, BindingPattern, BindingPatternKind, CallExpression, Class, ClassBody,
ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration, Expression,
FormalParameter, Function, IdentifierReference, MemberExpression, ModuleDeclaration,
NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey,
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
VariableDeclarator,
ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration,
ExportDefaultDeclarationKind, Expression, FormalParameter, Function, IdentifierReference,
MemberExpression, ModuleDeclaration, NewExpression, ParenthesizedExpression,
PrivateFieldExpression, Program, PropertyKey, SimpleAssignmentTarget, Statement,
StaticMemberExpression, ThisExpression, VariableDeclarator,
},
AstKind,
};
@ -18,7 +18,10 @@ use rustc_hash::FxHashSet;
use crate::{
ast_util::{get_declaration_of_variable, get_symbol_id_of_variable},
utils::{calculate_binary_operation, get_write_expr, has_pure_notation, no_effects, Value},
utils::{
calculate_binary_operation, get_leading_tree_shaking_comment, get_write_expr,
has_pure_notation, no_effects, Value, COMMENT_NO_SIDE_EFFECT_WHEN_CALLED,
},
LintContext,
};
@ -80,15 +83,25 @@ impl<'a> ListenerMap for Statement<'a> {
arg.report_effects(options);
}
}
Self::ModuleDeclaration(decl) => {
if matches!(
&**decl,
ModuleDeclaration::ExportAllDeclaration(_)
| ModuleDeclaration::ImportDeclaration(_)
) {
Self::ModuleDeclaration(decl) => match &**decl {
ModuleDeclaration::ExportAllDeclaration(_)
| 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);
}
}
expr.report_effects(options);
}
}
_ => {}
},
Self::TryStatement(stmt) => {
stmt.block.body.iter().for_each(|stmt| stmt.report_effects(options));
stmt.handler.iter().for_each(|handler| {

View file

@ -181,15 +181,15 @@ fn test() {
"const x = ()=>{}; do x(); while(true)",
// EmptyStatement
";",
// // ExportAllDeclaration
// r#"export * from "import""#,
// // ExportDefaultDeclaration
// "export default ext",
// "const x = ext; export default x",
// "export default function(){}",
// "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(){}",
// ExportAllDeclaration
r#"export * from "import""#,
// ExportDefaultDeclaration
"export default ext",
"const x = ext; export default x",
"export default function(){}",
"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()}",
@ -456,10 +456,10 @@ fn test() {
"do {} while(ext())",
"do ext(); while(true)",
"do {ext()} while(true)",
// // ExportDefaultDeclaration
// "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",
// ExportDefaultDeclaration
"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",

View file

@ -368,6 +368,24 @@ 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:16]
1 │ export default 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:63]
1 │ export default /* tree-shaking no-side-effects-when-called */ 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 default /* 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

@ -1,4 +1,4 @@
use oxc_ast::{ast::Expression, AstKind};
use oxc_ast::{ast::Expression, AstKind, CommentKind};
use oxc_semantic::AstNodeId;
use oxc_span::Span;
use oxc_syntax::operator::BinaryOperator;
@ -88,6 +88,80 @@ pub fn has_pure_notation(span: Span, ctx: &LintContext) -> bool {
raw.contains("@__PURE__") || raw.contains("#__PURE__")
}
const TREE_SHAKING_COMMENT_ID: &str = "tree-shaking";
pub 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)
}
/// Get the nearest comment before the `span`, return `None` if no leading comment is founded.
///
/// # Examples
/// ```
/// /* valid comment for `a` */ let a = 1;
///
/// // valid comment for `b`
/// let b = 1;
///
/// // valid comment for `c`
///
///
/// let c = 1;
///
/// let d = 1; /* invalid comment for `e` */
/// let e = 2
/// ```
pub fn get_leading_tree_shaking_comment<'a>(span: Span, ctx: &LintContext<'a>) -> Option<&'a str> {
let (start, comment) = ctx.semantic().trivias().comments_range(..span.start).next_back()?;
let comment_text = {
let span = Span::new(*start, comment.end);
span.source_text(ctx.source_text())
};
if !is_tree_shaking_comment(comment_text) {
return None;
}
// If there are non-whitespace characters between the `comment`` and the `span`,
// we treat the `comment` not belongs to the `span`.
let only_whitespace = ctx.source_text()[comment.end as usize..span.start as usize]
.strip_prefix("*/") // for multi-line comment
.is_some_and(|s| s.trim().is_empty());
if !only_whitespace {
return None;
}
// Next step, we need make sure it's not the trailing comment of the previous line.
let mut current_line_start = span.start as usize;
for c in ctx.source_text()[..span.start as usize].chars().rev() {
if c == '\n' {
break;
}
current_line_start -= c.len_utf8();
}
let Ok(current_line_start) = u32::try_from(current_line_start) else {
return None;
};
if comment.end < current_line_start {
let previous_line =
ctx.source_text()[..comment.end as usize].lines().next_back().unwrap_or("");
let nothing_before_comment = previous_line
.trim()
.strip_prefix(if comment.kind == CommentKind::SingleLine { "//" } else { "/*" })
.is_some_and(|s| s.trim().is_empty());
if !nothing_before_comment {
return None;
}
}
Some(comment_text)
}
/// Port from <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/463fa1f0bef7caa2b231a38b9c3557051f506c92/src/rules/no-side-effects-in-initialization.ts#L136-L161>
/// <https://tc39.es/ecma262/#sec-evaluatestringornumericbinaryexpression>
pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value) -> Value {