mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +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::{
|
ast::{
|
||||||
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentTarget,
|
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentTarget,
|
||||||
BinaryExpression, BindingPattern, BindingPatternKind, CallExpression, Class, ClassBody,
|
BinaryExpression, BindingPattern, BindingPatternKind, CallExpression, Class, ClassBody,
|
||||||
ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration, Expression,
|
ClassElement, ComputedMemberExpression, ConditionalExpression, Declaration,
|
||||||
FormalParameter, Function, IdentifierReference, MemberExpression, ModuleDeclaration,
|
ExportDefaultDeclarationKind, Expression, FormalParameter, Function, IdentifierReference,
|
||||||
NewExpression, ParenthesizedExpression, PrivateFieldExpression, Program, PropertyKey,
|
MemberExpression, ModuleDeclaration, NewExpression, ParenthesizedExpression,
|
||||||
SimpleAssignmentTarget, Statement, StaticMemberExpression, ThisExpression,
|
PrivateFieldExpression, Program, PropertyKey, SimpleAssignmentTarget, Statement,
|
||||||
VariableDeclarator,
|
StaticMemberExpression, ThisExpression, VariableDeclarator,
|
||||||
},
|
},
|
||||||
AstKind,
|
AstKind,
|
||||||
};
|
};
|
||||||
|
|
@ -18,7 +18,10 @@ use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast_util::{get_declaration_of_variable, get_symbol_id_of_variable},
|
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,
|
LintContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -80,15 +83,25 @@ impl<'a> ListenerMap for Statement<'a> {
|
||||||
arg.report_effects(options);
|
arg.report_effects(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::ModuleDeclaration(decl) => {
|
Self::ModuleDeclaration(decl) => match &**decl {
|
||||||
if matches!(
|
|
||||||
&**decl,
|
|
||||||
ModuleDeclaration::ExportAllDeclaration(_)
|
ModuleDeclaration::ExportAllDeclaration(_)
|
||||||
| ModuleDeclaration::ImportDeclaration(_)
|
| ModuleDeclaration::ImportDeclaration(_) => {
|
||||||
) {
|
|
||||||
no_effects();
|
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) => {
|
Self::TryStatement(stmt) => {
|
||||||
stmt.block.body.iter().for_each(|stmt| stmt.report_effects(options));
|
stmt.block.body.iter().for_each(|stmt| stmt.report_effects(options));
|
||||||
stmt.handler.iter().for_each(|handler| {
|
stmt.handler.iter().for_each(|handler| {
|
||||||
|
|
|
||||||
|
|
@ -181,15 +181,15 @@ fn test() {
|
||||||
"const x = ()=>{}; do x(); while(true)",
|
"const x = ()=>{}; do x(); while(true)",
|
||||||
// EmptyStatement
|
// EmptyStatement
|
||||||
";",
|
";",
|
||||||
// // ExportAllDeclaration
|
// ExportAllDeclaration
|
||||||
// r#"export * from "import""#,
|
r#"export * from "import""#,
|
||||||
// // ExportDefaultDeclaration
|
// ExportDefaultDeclaration
|
||||||
// "export default ext",
|
"export default ext",
|
||||||
// "const x = ext; export default x",
|
"const x = ext; export default x",
|
||||||
// "export default function(){}",
|
"export default function(){}",
|
||||||
// "export default (function(){})",
|
"export default (function(){})",
|
||||||
// "const x = function(){}; export default /* tree-shaking no-side-effects-when-called */ x",
|
"const x = function(){}; export default /* tree-shaking no-side-effects-when-called */ x",
|
||||||
// "export default /* tree-shaking no-side-effects-when-called */ function(){}",
|
"export default /* tree-shaking no-side-effects-when-called */ function(){}",
|
||||||
// // ExportNamedDeclaration
|
// // ExportNamedDeclaration
|
||||||
// "export const x = ext",
|
// "export const x = ext",
|
||||||
// "export function x(){ext()}",
|
// "export function x(){ext()}",
|
||||||
|
|
@ -456,10 +456,10 @@ fn test() {
|
||||||
"do {} while(ext())",
|
"do {} while(ext())",
|
||||||
"do ext(); while(true)",
|
"do ext(); while(true)",
|
||||||
"do {ext()} while(true)",
|
"do {ext()} while(true)",
|
||||||
// // ExportDefaultDeclaration
|
// ExportDefaultDeclaration
|
||||||
// "export default ext()",
|
"export default ext()",
|
||||||
// "export default /* tree-shaking no-side-effects-when-called */ ext",
|
"export default /* tree-shaking no-side-effects-when-called */ ext",
|
||||||
// "const x = ext; export default /* tree-shaking no-side-effects-when-called */ x",
|
"const x = ext; export default /* tree-shaking no-side-effects-when-called */ x",
|
||||||
// // ExportNamedDeclaration
|
// // ExportNamedDeclaration
|
||||||
// "export const x = ext()",
|
// "export const x = ext()",
|
||||||
// "export const /* tree-shaking no-side-effects-when-called */ 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`
|
⚠ 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]
|
╭─[no_side_effects_in_initialization.tsx:1:5]
|
||||||
1 │ if (ext()>0){}
|
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_semantic::AstNodeId;
|
||||||
use oxc_span::Span;
|
use oxc_span::Span;
|
||||||
use oxc_syntax::operator::BinaryOperator;
|
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__")
|
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>
|
/// 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>
|
/// <https://tc39.es/ecma262/#sec-evaluatestringornumericbinaryexpression>
|
||||||
pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value) -> Value {
|
pub fn calculate_binary_operation(op: BinaryOperator, left: Value, right: Value) -> Value {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue