mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/tree-shaking): support options (#3504)
This commit is contained in:
parent
568c9c54c4
commit
6b39654c80
4 changed files with 353 additions and 66 deletions
|
|
@ -15,46 +15,20 @@ use oxc_ast::{
|
||||||
},
|
},
|
||||||
AstKind,
|
AstKind,
|
||||||
};
|
};
|
||||||
use oxc_semantic::{AstNode, SymbolId};
|
use oxc_semantic::{AstNode, AstNodeId};
|
||||||
use oxc_span::{GetSpan, Span};
|
use oxc_span::{GetSpan, Span};
|
||||||
use oxc_syntax::operator::{LogicalOperator, UnaryOperator};
|
use oxc_syntax::operator::{LogicalOperator, UnaryOperator};
|
||||||
use rustc_hash::FxHashSet;
|
|
||||||
use std::{cell::Cell, cell::RefCell};
|
|
||||||
|
|
||||||
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::{
|
utils::{
|
||||||
calculate_binary_operation, calculate_logical_operation, calculate_unary_operation,
|
calculate_binary_operation, calculate_logical_operation, calculate_unary_operation,
|
||||||
get_write_expr, has_comment_about_side_effect_check, has_pure_notation, is_pure_function,
|
get_write_expr, has_comment_about_side_effect_check, has_pure_notation,
|
||||||
no_effects, FunctionName, Value,
|
is_function_side_effect_free, is_local_variable_a_whitelisted_module, is_pure_function,
|
||||||
|
no_effects, FunctionName, NodeListenerOptions, Value,
|
||||||
},
|
},
|
||||||
LintContext,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct NodeListenerOptions<'a, 'b> {
|
|
||||||
checked_mutated_nodes: RefCell<FxHashSet<SymbolId>>,
|
|
||||||
ctx: &'b LintContext<'a>,
|
|
||||||
has_valid_this: Cell<bool>,
|
|
||||||
called_with_new: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> NodeListenerOptions<'a, 'b> {
|
|
||||||
fn insert_mutated_node(&self, symbol_id: SymbolId) -> bool {
|
|
||||||
self.checked_mutated_nodes.borrow_mut().insert(symbol_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> NodeListenerOptions<'a, 'b> {
|
|
||||||
pub fn new(ctx: &'b LintContext<'a>) -> Self {
|
|
||||||
Self {
|
|
||||||
checked_mutated_nodes: RefCell::new(FxHashSet::default()),
|
|
||||||
ctx,
|
|
||||||
has_valid_this: Cell::new(false),
|
|
||||||
called_with_new: Cell::new(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ListenerMap {
|
pub trait ListenerMap {
|
||||||
fn report_effects(&self, _options: &NodeListenerOptions) {}
|
fn report_effects(&self, _options: &NodeListenerOptions) {}
|
||||||
fn report_effects_when_assigned(&self, _options: &NodeListenerOptions) {}
|
fn report_effects_when_assigned(&self, _options: &NodeListenerOptions) {}
|
||||||
|
|
@ -276,21 +250,28 @@ impl<'a> ListenerMap for AstNode<'a> {
|
||||||
class.report_effects_when_called(options);
|
class.report_effects_when_called(options);
|
||||||
}
|
}
|
||||||
AstKind::ImportDefaultSpecifier(specifier) => {
|
AstKind::ImportDefaultSpecifier(specifier) => {
|
||||||
if !has_comment_about_side_effect_check(specifier.span, options.ctx) {
|
report_on_imported_call(
|
||||||
options.ctx.diagnostic(super::call_import(specifier.span));
|
specifier.local.span,
|
||||||
}
|
&specifier.local.name,
|
||||||
|
self.id(),
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
AstKind::ImportSpecifier(specifier) => {
|
AstKind::ImportSpecifier(specifier) => {
|
||||||
let span = specifier.local.span;
|
report_on_imported_call(
|
||||||
if !has_comment_about_side_effect_check(span, options.ctx) {
|
specifier.local.span,
|
||||||
options.ctx.diagnostic(super::call_import(span));
|
&specifier.local.name,
|
||||||
}
|
self.id(),
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
AstKind::ImportNamespaceSpecifier(specifier) => {
|
AstKind::ImportNamespaceSpecifier(specifier) => {
|
||||||
let span = specifier.local.span;
|
report_on_imported_call(
|
||||||
if !has_comment_about_side_effect_check(span, options.ctx) {
|
specifier.local.span,
|
||||||
options.ctx.diagnostic(super::call_import(span));
|
&specifier.local.name,
|
||||||
}
|
self.id(),
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -324,6 +305,24 @@ impl<'a> ListenerMap for AstNode<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn report_on_imported_call(
|
||||||
|
span: Span,
|
||||||
|
name: &str,
|
||||||
|
node_id: AstNodeId,
|
||||||
|
options: &NodeListenerOptions,
|
||||||
|
) {
|
||||||
|
if has_comment_about_side_effect_check(span, options.ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(AstKind::ImportDeclaration(decl)) = options.ctx.nodes().parent_kind(node_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if is_function_side_effect_free(name, &decl.source.value, options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.ctx.diagnostic(super::call_import(span));
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> ListenerMap for Declaration<'a> {
|
impl<'a> ListenerMap for Declaration<'a> {
|
||||||
fn report_effects(&self, options: &NodeListenerOptions) {
|
fn report_effects(&self, options: &NodeListenerOptions) {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -1053,11 +1052,7 @@ impl<'a> ListenerMap for CallExpression<'a> {
|
||||||
let ctx = options.ctx;
|
let ctx = options.ctx;
|
||||||
if let Expression::Identifier(ident) = &self.callee {
|
if let Expression::Identifier(ident) = &self.callee {
|
||||||
if let Some(node) = get_declaration_of_variable(ident, ctx) {
|
if let Some(node) = get_declaration_of_variable(ident, ctx) {
|
||||||
let Some(parent) = ctx.nodes().parent_kind(node.id()) else {
|
if is_local_variable_a_whitelisted_module(node, ident.name.as_str(), options) {
|
||||||
return;
|
|
||||||
};
|
|
||||||
// TODO: `isLocalVariableAWhitelistedModule`
|
|
||||||
if matches!(parent, AstKind::ImportDeclaration(_)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
options.ctx.diagnostic(super::call_return_value(self.span));
|
options.ctx.diagnostic(super::call_return_value(self.span));
|
||||||
|
|
@ -1120,7 +1115,7 @@ impl<'a> ListenerMap for IdentifierReference<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
|
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
|
||||||
if is_pure_function(&FunctionName::Identifier(self), options.ctx) {
|
if is_pure_function(&FunctionName::Identifier(self), options) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1249,31 +1244,43 @@ impl<'a> ListenerMap for StaticMemberExpression<'a> {
|
||||||
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
|
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
|
||||||
self.report_effects(options);
|
self.report_effects(options);
|
||||||
|
|
||||||
let mut node = &self.object;
|
let mut root_member_expr = &self.object;
|
||||||
loop {
|
loop {
|
||||||
match node {
|
match root_member_expr {
|
||||||
Expression::ComputedMemberExpression(expr) => {
|
Expression::ComputedMemberExpression(expr) => {
|
||||||
node = &expr.object;
|
root_member_expr = &expr.object;
|
||||||
}
|
}
|
||||||
Expression::StaticMemberExpression(expr) => node = &expr.object,
|
Expression::StaticMemberExpression(expr) => root_member_expr = &expr.object,
|
||||||
Expression::PrivateInExpression(expr) => node = &expr.right,
|
Expression::PrivateInExpression(expr) => root_member_expr = &expr.right,
|
||||||
_ => {
|
_ => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Expression::Identifier(ident) = node else {
|
let Expression::Identifier(ident) = root_member_expr else {
|
||||||
options.ctx.diagnostic(super::call_member(node.span()));
|
options.ctx.diagnostic(super::call_member(root_member_expr.span()));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if get_declaration_of_variable(ident, options.ctx)
|
let Some(node) = get_declaration_of_variable(ident, options.ctx) else {
|
||||||
.is_some_and(|_| !has_pure_notation(self.span, options.ctx))
|
// If the variable is not declared, it is a global variable.
|
||||||
|| !is_pure_function(&FunctionName::StaticMemberExpr(self), options.ctx)
|
// `ext.x()`
|
||||||
{
|
if !is_pure_function(&FunctionName::StaticMemberExpr(self), options) {
|
||||||
options.ctx.diagnostic(super::call_member(self.span));
|
options.ctx.diagnostic(super::call_member(self.span));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_local_variable_a_whitelisted_module(node, &ident.name, options) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_pure_notation(self.span, options.ctx) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.ctx.diagnostic(super::call_member(self.span));
|
||||||
}
|
}
|
||||||
fn report_effects_when_assigned(&self, options: &NodeListenerOptions) {
|
fn report_effects_when_assigned(&self, options: &NodeListenerOptions) {
|
||||||
self.report_effects(options);
|
self.report_effects(options);
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,15 @@ use oxc_ast::AstKind;
|
||||||
use oxc_diagnostics::OxcDiagnostic;
|
use oxc_diagnostics::OxcDiagnostic;
|
||||||
use oxc_macros::declare_oxc_lint;
|
use oxc_macros::declare_oxc_lint;
|
||||||
use oxc_span::Span;
|
use oxc_span::Span;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{context::LintContext, rule::Rule};
|
use crate::{
|
||||||
|
context::LintContext,
|
||||||
|
rule::Rule,
|
||||||
|
utils::{ModuleFunctions, NodeListenerOptions, WhitelistModule},
|
||||||
|
};
|
||||||
|
|
||||||
use self::listener_map::{ListenerMap, NodeListenerOptions};
|
use self::listener_map::ListenerMap;
|
||||||
|
|
||||||
mod listener_map;
|
mod listener_map;
|
||||||
|
|
||||||
|
|
@ -75,7 +80,21 @@ fn throw(span0: Span) -> OxcDiagnostic {
|
||||||
|
|
||||||
/// <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/master/src/rules/no-side-effects-in-initialization.ts>
|
/// <https://github.com/lukastaegert/eslint-plugin-tree-shaking/blob/master/src/rules/no-side-effects-in-initialization.ts>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct NoSideEffectsInInitialization;
|
pub struct NoSideEffectsInInitialization(Box<NoSideEffectsInInitiallizationOptions>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for NoSideEffectsInInitialization {
|
||||||
|
type Target = NoSideEffectsInInitiallizationOptions;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct NoSideEffectsInInitiallizationOptions {
|
||||||
|
functions: Vec<String>,
|
||||||
|
modules: Vec<WhitelistModule>,
|
||||||
|
}
|
||||||
|
|
||||||
declare_oxc_lint!(
|
declare_oxc_lint!(
|
||||||
/// ### What it does
|
/// ### What it does
|
||||||
|
|
@ -94,17 +113,118 @@ declare_oxc_lint!(
|
||||||
/// const x = { [globalFunction()]: "myString" }; // Cannot determine side-effects of calling global function
|
/// const x = { [globalFunction()]: "myString" }; // Cannot determine side-effects of calling global function
|
||||||
/// export default 42;
|
/// export default 42;
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### Options
|
||||||
|
///
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "rules": {
|
||||||
|
/// "tree-shaking/no-side-effects-in-initialization": [
|
||||||
|
/// 2,
|
||||||
|
/// {
|
||||||
|
/// "noSideEffectsWhenCalled": [
|
||||||
|
/// // If you want to mark a function call as side-effect free
|
||||||
|
/// { "function": "Object.freeze" },
|
||||||
|
/// {
|
||||||
|
/// "module": "react",
|
||||||
|
/// "functions": ["createContext", "createRef"]
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "module": "zod",
|
||||||
|
/// "functions": ["array", "string", "nativeEnum", "number", "object", "optional"]
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "module": "my/local/module",
|
||||||
|
/// "functions": ["foo", "bar", "baz"]
|
||||||
|
/// },
|
||||||
|
/// // If you want to whitelist all functions of a module
|
||||||
|
/// {
|
||||||
|
/// "module": "lodash",
|
||||||
|
/// "functions": "*"
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### Magic Comments
|
||||||
|
///
|
||||||
|
/// Besides the configuration, you can also use magic comments to mark a function call as side effect free.
|
||||||
|
///
|
||||||
|
/// By default, imported functions are assumed to have side-effects, unless they are marked with a magic comment:
|
||||||
|
///
|
||||||
|
/// ```js
|
||||||
|
/// import { /* tree-shaking no-side-effects-when-called */ x } from "./some-file";
|
||||||
|
/// x();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// `@__PURE__` is also supported:
|
||||||
|
///
|
||||||
|
/// ```js
|
||||||
|
/// import {x} from "./some-file";
|
||||||
|
/// /*@__PURE__*/ x();
|
||||||
|
/// ```
|
||||||
NoSideEffectsInInitialization,
|
NoSideEffectsInInitialization,
|
||||||
nursery
|
nursery
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Rule for NoSideEffectsInInitialization {
|
impl Rule for NoSideEffectsInInitialization {
|
||||||
|
fn from_configuration(value: serde_json::Value) -> Self {
|
||||||
|
let mut functions = vec![];
|
||||||
|
let mut modules = vec![];
|
||||||
|
|
||||||
|
if let Value::Array(arr) = value {
|
||||||
|
for obj in arr {
|
||||||
|
let Value::Object(obj) = obj else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// { "function": "Object.freeze" }
|
||||||
|
if let Some(name) = obj.get("function").and_then(Value::as_str) {
|
||||||
|
functions.push(name.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// { "module": "react", "functions": ["createContext", "createRef"] }
|
||||||
|
// { "module": "react", "functions": "*" }
|
||||||
|
if let Some(name) = obj.get("module").and_then(Value::as_str) {
|
||||||
|
let functions = match obj.get("functions") {
|
||||||
|
Some(Value::Array(arr)) => {
|
||||||
|
let val = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(String::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Some(ModuleFunctions::Specific(val))
|
||||||
|
}
|
||||||
|
Some(Value::String(str)) => {
|
||||||
|
if str == "*" {
|
||||||
|
Some(ModuleFunctions::All)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(functions) = functions {
|
||||||
|
modules.push(WhitelistModule { name: name.to_string(), functions });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(Box::new(NoSideEffectsInInitiallizationOptions { functions, modules }))
|
||||||
|
}
|
||||||
fn run_once(&self, ctx: &LintContext) {
|
fn run_once(&self, ctx: &LintContext) {
|
||||||
let Some(root) = ctx.nodes().root_node() else {
|
let Some(root) = ctx.nodes().root_node() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let AstKind::Program(program) = root.kind() else { unreachable!() };
|
let AstKind::Program(program) = root.kind() else { unreachable!() };
|
||||||
let node_listener_options = NodeListenerOptions::new(ctx);
|
let node_listener_options = NodeListenerOptions::new(ctx)
|
||||||
|
.with_whitelist_functions(&self.functions)
|
||||||
|
.with_whitelist_modules(&self.modules);
|
||||||
program.report_effects(&node_listener_options);
|
program.report_effects(&node_listener_options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -670,5 +790,46 @@ fn test() {
|
||||||
"function* x(){yield ext()}; x()",
|
"function* x(){yield ext()}; x()",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// test options
|
||||||
|
let pass_with_options = vec![
|
||||||
|
(
|
||||||
|
"Object.freeze({})",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{ "function": "Object.freeze" },
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"import {createContext, createRef} from 'react'; createContext(); createRef();",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{ "module": "react", "functions": ["createContext", "createRef"] },
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"import _ from 'lodash'; _.cloneDeep({});",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{ "module": "lodash", "functions": "*" },
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"import * as React from 'react'; React.createRef();",
|
||||||
|
Some(serde_json::json!([
|
||||||
|
{ "module": "react", "functions": "*" },
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail_with_options = vec![
|
||||||
|
("Object.freeze({})", None),
|
||||||
|
("import {createContext, createRef} from 'react'; createContext(); createRef();", None),
|
||||||
|
("import _ from 'lodash'; _.cloneDeep({});", None),
|
||||||
|
("import * as React from 'react'; React.createRef();", None),
|
||||||
|
];
|
||||||
|
|
||||||
|
let pass =
|
||||||
|
pass.into_iter().map(|case| (case, None)).chain(pass_with_options).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let fail =
|
||||||
|
fail.into_iter().map(|case| (case, None)).chain(fail_with_options).collect::<Vec<_>>();
|
||||||
|
|
||||||
Tester::new(NoSideEffectsInInitialization::NAME, pass, fail).test_and_snapshot();
|
Tester::new(NoSideEffectsInInitialization::NAME, pass, fail).test_and_snapshot();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1303,3 +1303,33 @@ expression: no_side_effects_in_initialization
|
||||||
1 │ function* x(){yield ext()}; x()
|
1 │ function* x(){yield ext()}; x()
|
||||||
· ───
|
· ───
|
||||||
╰────
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function
|
||||||
|
╭─[no_side_effects_in_initialization.tsx:1:1]
|
||||||
|
1 │ Object.freeze({})
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling imported function
|
||||||
|
╭─[no_side_effects_in_initialization.tsx:1:9]
|
||||||
|
1 │ import {createContext, createRef} from 'react'; createContext(); createRef();
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling imported function
|
||||||
|
╭─[no_side_effects_in_initialization.tsx:1:24]
|
||||||
|
1 │ import {createContext, createRef} from 'react'; createContext(); createRef();
|
||||||
|
· ─────────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function
|
||||||
|
╭─[no_side_effects_in_initialization.tsx:1:25]
|
||||||
|
1 │ import _ from 'lodash'; _.cloneDeep({});
|
||||||
|
· ───────────
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-tree-shaking(no-side-effects-in-initialization): Cannot determine side-effects of calling member function
|
||||||
|
╭─[no_side_effects_in_initialization.tsx:1:33]
|
||||||
|
1 │ import * as React from 'react'; React.createRef();
|
||||||
|
· ───────────────
|
||||||
|
╰────
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use oxc_ast::{
|
use oxc_ast::{
|
||||||
ast::{Expression, IdentifierReference, StaticMemberExpression},
|
ast::{Expression, IdentifierReference, StaticMemberExpression},
|
||||||
AstKind, CommentKind,
|
AstKind, CommentKind,
|
||||||
};
|
};
|
||||||
use oxc_semantic::AstNodeId;
|
use oxc_semantic::{AstNode, AstNodeId, SymbolId};
|
||||||
use oxc_span::{CompactStr, GetSpan, Span};
|
use oxc_span::{CompactStr, GetSpan, Span};
|
||||||
use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator};
|
use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
@ -12,6 +14,50 @@ use crate::LintContext;
|
||||||
|
|
||||||
mod pure_functions;
|
mod pure_functions;
|
||||||
|
|
||||||
|
pub struct NodeListenerOptions<'a, 'b> {
|
||||||
|
pub checked_mutated_nodes: RefCell<FxHashSet<SymbolId>>,
|
||||||
|
pub ctx: &'b LintContext<'a>,
|
||||||
|
pub has_valid_this: Cell<bool>,
|
||||||
|
pub called_with_new: Cell<bool>,
|
||||||
|
pub whitelist_modules: Option<&'b Vec<WhitelistModule>>,
|
||||||
|
pub whitelist_functions: Option<&'b Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> NodeListenerOptions<'a, 'b> {
|
||||||
|
pub fn new(ctx: &'b LintContext<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
checked_mutated_nodes: RefCell::new(FxHashSet::default()),
|
||||||
|
ctx,
|
||||||
|
has_valid_this: Cell::new(false),
|
||||||
|
called_with_new: Cell::new(false),
|
||||||
|
whitelist_modules: None,
|
||||||
|
whitelist_functions: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_whitelist_modules(self, whitelist_modules: &'b Vec<WhitelistModule>) -> Self {
|
||||||
|
Self { whitelist_modules: Some(whitelist_modules), ..self }
|
||||||
|
}
|
||||||
|
pub fn with_whitelist_functions(self, whitelist_functions: &'b Vec<String>) -> Self {
|
||||||
|
Self { whitelist_functions: Some(whitelist_functions), ..self }
|
||||||
|
}
|
||||||
|
pub fn insert_mutated_node(&self, symbol_id: SymbolId) -> bool {
|
||||||
|
self.checked_mutated_nodes.borrow_mut().insert(symbol_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct WhitelistModule {
|
||||||
|
pub name: String,
|
||||||
|
pub functions: ModuleFunctions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub enum ModuleFunctions {
|
||||||
|
#[default]
|
||||||
|
All,
|
||||||
|
Specific(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Boolean(bool),
|
Boolean(bool),
|
||||||
|
|
@ -127,11 +173,16 @@ impl GetSpan for FunctionName<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_pure_function(function_name: &FunctionName, ctx: &LintContext) -> bool {
|
pub fn is_pure_function(function_name: &FunctionName, options: &NodeListenerOptions) -> bool {
|
||||||
if has_pure_notation(function_name.span(), ctx) {
|
if has_pure_notation(function_name.span(), options.ctx) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let name = flatten_member_expr_if_possible(function_name);
|
let name = flatten_member_expr_if_possible(function_name);
|
||||||
|
|
||||||
|
if options.whitelist_functions.is_some_and(|whitelist| whitelist.contains(&name.to_string())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
PURE_FUNCTIONS_SET.contains(name.as_str())
|
PURE_FUNCTIONS_SET.contains(name.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,6 +302,44 @@ pub fn get_leading_tree_shaking_comment<'a>(span: Span, ctx: &LintContext<'a>) -
|
||||||
Some(comment_text)
|
Some(comment_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_local_variable_a_whitelisted_module(
|
||||||
|
node: &AstNode,
|
||||||
|
name: &str,
|
||||||
|
options: &NodeListenerOptions,
|
||||||
|
) -> bool {
|
||||||
|
let Some(AstKind::ImportDeclaration(parent)) = options.ctx.nodes().parent_kind(node.id())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let module_name = parent.source.value.as_str();
|
||||||
|
is_function_side_effect_free(name, module_name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_function_side_effect_free(
|
||||||
|
name: &str,
|
||||||
|
module_name: &str,
|
||||||
|
options: &NodeListenerOptions,
|
||||||
|
) -> bool {
|
||||||
|
let Some(whitelist_modules) = options.whitelist_modules else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for module in whitelist_modules {
|
||||||
|
let is_module_match =
|
||||||
|
module.name == module_name || module.name == "#local" && module_name.starts_with('.');
|
||||||
|
|
||||||
|
if is_module_match {
|
||||||
|
match &module.functions {
|
||||||
|
ModuleFunctions::All => return true,
|
||||||
|
ModuleFunctions::Specific(functions) => {
|
||||||
|
return functions.contains(&name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// 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>
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue