mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter/tree-shaking): support ExportDefaultDeclaration (#3052)
This commit is contained in:
parent
df7b9ee8da
commit
1a1ba11a3b
4 changed files with 132 additions and 27 deletions
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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){}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue