mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +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,
|
||||
};
|
||||
use oxc_semantic::{AstNode, SymbolId};
|
||||
use oxc_semantic::{AstNode, AstNodeId};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_syntax::operator::{LogicalOperator, UnaryOperator};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{cell::Cell, cell::RefCell};
|
||||
|
||||
use crate::{
|
||||
ast_util::{get_declaration_of_variable, get_symbol_id_of_variable},
|
||||
utils::{
|
||||
calculate_binary_operation, calculate_logical_operation, calculate_unary_operation,
|
||||
get_write_expr, has_comment_about_side_effect_check, has_pure_notation, is_pure_function,
|
||||
no_effects, FunctionName, Value,
|
||||
get_write_expr, has_comment_about_side_effect_check, has_pure_notation,
|
||||
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 {
|
||||
fn report_effects(&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);
|
||||
}
|
||||
AstKind::ImportDefaultSpecifier(specifier) => {
|
||||
if !has_comment_about_side_effect_check(specifier.span, options.ctx) {
|
||||
options.ctx.diagnostic(super::call_import(specifier.span));
|
||||
}
|
||||
report_on_imported_call(
|
||||
specifier.local.span,
|
||||
&specifier.local.name,
|
||||
self.id(),
|
||||
options,
|
||||
);
|
||||
}
|
||||
AstKind::ImportSpecifier(specifier) => {
|
||||
let span = specifier.local.span;
|
||||
if !has_comment_about_side_effect_check(span, options.ctx) {
|
||||
options.ctx.diagnostic(super::call_import(span));
|
||||
}
|
||||
report_on_imported_call(
|
||||
specifier.local.span,
|
||||
&specifier.local.name,
|
||||
self.id(),
|
||||
options,
|
||||
);
|
||||
}
|
||||
AstKind::ImportNamespaceSpecifier(specifier) => {
|
||||
let span = specifier.local.span;
|
||||
if !has_comment_about_side_effect_check(span, options.ctx) {
|
||||
options.ctx.diagnostic(super::call_import(span));
|
||||
}
|
||||
report_on_imported_call(
|
||||
specifier.local.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> {
|
||||
fn report_effects(&self, options: &NodeListenerOptions) {
|
||||
match self {
|
||||
|
|
@ -1053,11 +1052,7 @@ impl<'a> ListenerMap for CallExpression<'a> {
|
|||
let ctx = options.ctx;
|
||||
if let Expression::Identifier(ident) = &self.callee {
|
||||
if let Some(node) = get_declaration_of_variable(ident, ctx) {
|
||||
let Some(parent) = ctx.nodes().parent_kind(node.id()) else {
|
||||
return;
|
||||
};
|
||||
// TODO: `isLocalVariableAWhitelistedModule`
|
||||
if matches!(parent, AstKind::ImportDeclaration(_)) {
|
||||
if is_local_variable_a_whitelisted_module(node, ident.name.as_str(), options) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if is_pure_function(&FunctionName::Identifier(self), options.ctx) {
|
||||
if is_pure_function(&FunctionName::Identifier(self), options) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1249,31 +1244,43 @@ impl<'a> ListenerMap for StaticMemberExpression<'a> {
|
|||
fn report_effects_when_called(&self, options: &NodeListenerOptions) {
|
||||
self.report_effects(options);
|
||||
|
||||
let mut node = &self.object;
|
||||
let mut root_member_expr = &self.object;
|
||||
loop {
|
||||
match node {
|
||||
match root_member_expr {
|
||||
Expression::ComputedMemberExpression(expr) => {
|
||||
node = &expr.object;
|
||||
root_member_expr = &expr.object;
|
||||
}
|
||||
Expression::StaticMemberExpression(expr) => node = &expr.object,
|
||||
Expression::PrivateInExpression(expr) => node = &expr.right,
|
||||
Expression::StaticMemberExpression(expr) => root_member_expr = &expr.object,
|
||||
Expression::PrivateInExpression(expr) => root_member_expr = &expr.right,
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Expression::Identifier(ident) = node else {
|
||||
options.ctx.diagnostic(super::call_member(node.span()));
|
||||
let Expression::Identifier(ident) = root_member_expr else {
|
||||
options.ctx.diagnostic(super::call_member(root_member_expr.span()));
|
||||
return;
|
||||
};
|
||||
|
||||
if get_declaration_of_variable(ident, options.ctx)
|
||||
.is_some_and(|_| !has_pure_notation(self.span, options.ctx))
|
||||
|| !is_pure_function(&FunctionName::StaticMemberExpr(self), options.ctx)
|
||||
{
|
||||
options.ctx.diagnostic(super::call_member(self.span));
|
||||
let Some(node) = get_declaration_of_variable(ident, options.ctx) else {
|
||||
// If the variable is not declared, it is a global variable.
|
||||
// `ext.x()`
|
||||
if !is_pure_function(&FunctionName::StaticMemberExpr(self), options) {
|
||||
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) {
|
||||
self.report_effects(options);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ use oxc_ast::AstKind;
|
|||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
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;
|
||||
|
||||
|
|
@ -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>
|
||||
#[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!(
|
||||
/// ### What it does
|
||||
|
|
@ -94,17 +113,118 @@ declare_oxc_lint!(
|
|||
/// const x = { [globalFunction()]: "myString" }; // Cannot determine side-effects of calling global function
|
||||
/// 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,
|
||||
nursery
|
||||
);
|
||||
|
||||
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) {
|
||||
let Some(root) = ctx.nodes().root_node() else {
|
||||
return;
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -670,5 +790,46 @@ fn test() {
|
|||
"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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1303,3 +1303,33 @@ expression: no_side_effects_in_initialization
|
|||
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 oxc_ast::{
|
||||
ast::{Expression, IdentifierReference, StaticMemberExpression},
|
||||
AstKind, CommentKind,
|
||||
};
|
||||
use oxc_semantic::AstNodeId;
|
||||
use oxc_semantic::{AstNode, AstNodeId, SymbolId};
|
||||
use oxc_span::{CompactStr, GetSpan, Span};
|
||||
use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
|
@ -12,6 +14,50 @@ use crate::LintContext;
|
|||
|
||||
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)]
|
||||
pub enum Value {
|
||||
Boolean(bool),
|
||||
|
|
@ -127,11 +173,16 @@ impl GetSpan for FunctionName<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_pure_function(function_name: &FunctionName, ctx: &LintContext) -> bool {
|
||||
if has_pure_notation(function_name.span(), ctx) {
|
||||
pub fn is_pure_function(function_name: &FunctionName, options: &NodeListenerOptions) -> bool {
|
||||
if has_pure_notation(function_name.span(), options.ctx) {
|
||||
return true;
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +302,44 @@ pub fn get_leading_tree_shaking_comment<'a>(span: Span, ctx: &LintContext<'a>) -
|
|||
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>
|
||||
/// <https://tc39.es/ecma262/#sec-evaluatestringornumericbinaryexpression>
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue