mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(task): init eslint-plugin-tree-shaking rule (#2662)
This commit is contained in:
parent
588e94604c
commit
f8e8af2a66
5 changed files with 732 additions and 38 deletions
|
|
@ -330,6 +330,10 @@ mod nextjs {
|
|||
pub mod no_unwanted_polyfillio;
|
||||
}
|
||||
|
||||
mod tree_shaking {
|
||||
pub mod no_side_effects_in_initialization;
|
||||
}
|
||||
|
||||
oxc_macros::declare_all_lint_rules! {
|
||||
deepscan::bad_array_method_on_arguments,
|
||||
deepscan::bad_bitwise_operator,
|
||||
|
|
@ -622,4 +626,5 @@ oxc_macros::declare_all_lint_rules! {
|
|||
nextjs::no_document_import_in_page,
|
||||
nextjs::no_unwanted_polyfillio,
|
||||
nextjs::no_before_interactive_script_outside_document,
|
||||
tree_shaking::no_side_effects_in_initialization,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,607 @@
|
|||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error(
|
||||
"eslint-plugin-tree-shaking(no-side-effects-in-initialization): cannot determine side-effects"
|
||||
)]
|
||||
#[diagnostic(severity(warning), help(""))]
|
||||
struct NoSideEffectsInInitializationDiagnostic(#[label] pub Span);
|
||||
|
||||
/// <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;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Marks all side-effects in module initialization that will interfere with tree-shaking.
|
||||
///
|
||||
/// This plugin is intended as a means for library developers to identify patterns that will
|
||||
/// interfere with the tree-shaking algorithm of their module bundler (i.e. rollup or webpack).
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// myGlobal = 17; // Cannot determine side-effects of assignment to global variable
|
||||
/// const x = { [globalFunction()]: "myString" }; // Cannot determine side-effects of calling global function
|
||||
/// export default 42;
|
||||
/// ```
|
||||
NoSideEffectsInInitialization,
|
||||
nursery
|
||||
);
|
||||
|
||||
impl Rule for NoSideEffectsInInitialization {
|
||||
fn run<'a>(&self, _node: &AstNode<'a>, _ctx: &LintContext<'a>) {}
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// ArrayExpression
|
||||
"[]",
|
||||
"const x = []",
|
||||
"const x = [ext,ext]",
|
||||
"const x = [1,,2,]",
|
||||
// ArrayPattern
|
||||
"const [x] = []",
|
||||
"const [,x,] = []",
|
||||
// ArrowFunctionExpression
|
||||
"const x = a=>{a(); ext()}",
|
||||
// ArrowFunctionExpression when called
|
||||
"(()=>{})()",
|
||||
"(a=>{})()",
|
||||
"((...a)=>{})()",
|
||||
"(({a})=>{})()",
|
||||
// ArrowFunctionExpression when mutated
|
||||
"const x = ()=>{}; x.y = 1",
|
||||
// AssignmentExpression
|
||||
"var x;x = {}",
|
||||
"var x;x += 1",
|
||||
"const x = {}; x.y = 1",
|
||||
r#"const x = {}; x["y"] = 1"#,
|
||||
"function x(){this.y = 1}; const z = new x()",
|
||||
"let x = 1; x = 2 + 3",
|
||||
"let x; x = 2 + 3",
|
||||
// AssignmentPattern
|
||||
"const {x = ext} = {}",
|
||||
"const {x: y = ext} = {}",
|
||||
"const {[ext]: x = ext} = {}",
|
||||
"const x = ()=>{}, {y = x()} = {}",
|
||||
// BinaryExpression
|
||||
"const x = 1 + 2",
|
||||
"if (1-1) ext()",
|
||||
// BlockStatement
|
||||
"{}",
|
||||
"const x = ()=>{};{const x = ext}x()",
|
||||
"const x = ext;{const x = ()=>{}; x()}",
|
||||
// BreakStatement
|
||||
"while(true){break}",
|
||||
// CallExpression
|
||||
"(a=>{const y = a})(ext, ext)",
|
||||
"const x = ()=>{}, y = ()=>{}; x(y())",
|
||||
// CatchClause
|
||||
"try {} catch (error) {}",
|
||||
"const x = ()=>{}; try {} catch (error) {const x = ext}; x()",
|
||||
"const x = ext; try {} catch (error) {const x = ()=>{}; x()}",
|
||||
// ClassBody
|
||||
"class x {a(){ext()}}",
|
||||
// ClassBody when called
|
||||
"class x {a(){ext()}}; const y = new x()",
|
||||
"class x {constructor(){}}; const y = new x()",
|
||||
"class y{}; class x extends y{}; const z = new x()",
|
||||
// ClassDeclaration
|
||||
"class x extends ext {}",
|
||||
// ClassDeclaration when called
|
||||
"class x {}; const y = new x()",
|
||||
// ClassExpression
|
||||
"const x = class extends ext {}",
|
||||
// ClassExpression when called
|
||||
"const x = new (class {})()",
|
||||
// ClassProperty
|
||||
"class x {y}",
|
||||
"class x {y = 1}",
|
||||
"class x {y = ext()}",
|
||||
// ConditionalExpression
|
||||
"const x = ext ? 1 : 2",
|
||||
"const x = true ? 1 : ext()",
|
||||
"const x = false ? ext() : 2",
|
||||
"if (true ? false : true) ext()",
|
||||
"ext ? 1 : ext.x",
|
||||
"ext ? ext.x : 1",
|
||||
// ConditionalExpression when called
|
||||
"const x = ()=>{}, y = ()=>{};(ext ? x : y)()",
|
||||
"const x = ()=>{}; (true ? x : ext)()",
|
||||
"const x = ()=>{}; (false ? ext : x)()",
|
||||
// ContinueStatement
|
||||
"while(true){continue}",
|
||||
// DoWhileStatement
|
||||
"do {} while(true)",
|
||||
"do {} while(ext > 0)",
|
||||
"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(){}",
|
||||
// ExportNamedDeclaration
|
||||
"export const x = ext",
|
||||
"export function x(){ext()}",
|
||||
"const x = ext; export {x}",
|
||||
r#"export {x} from "import""#,
|
||||
r#"export {x as y} from "import""#,
|
||||
r#"export {x as default} from "import""#,
|
||||
"export const /* tree-shaking no-side-effects-when-called */ x = function(){}",
|
||||
"export function /* tree-shaking no-side-effects-when-called */ x(){}",
|
||||
"const x = function(){}; export {/* tree-shaking no-side-effects-when-called */ x}",
|
||||
// ExpressionStatement
|
||||
"const x = 1",
|
||||
// ForInStatement
|
||||
"for(const x in ext){x = 1}",
|
||||
"let x; for(x in ext){}",
|
||||
// ForStatement
|
||||
"for(let i = 0; i < 3; i++){i++}",
|
||||
"for(;;){}",
|
||||
// FunctionDeclaration
|
||||
"function x(a){a(); ext()}",
|
||||
// FunctionDeclaration when called
|
||||
"function x(){}; x()",
|
||||
"function x(a){}; x()",
|
||||
"function x(...a){}; x()",
|
||||
"function x({a}){}; x()",
|
||||
// FunctionDeclaration when mutated
|
||||
"function x(){}; x.y = 1",
|
||||
// FunctionExpression
|
||||
"const x = function (a){a(); ext()}",
|
||||
// FunctionExpression when called
|
||||
"(function (){}())",
|
||||
"(function (a){}())",
|
||||
"(function (...a){}())",
|
||||
"(function ({a}){}())",
|
||||
// Identifier
|
||||
"var x;x = 1",
|
||||
// Identifier when called
|
||||
"const x = ()=>{};x(ext)",
|
||||
"function x(){};x(ext)",
|
||||
"var x = ()=>{};x(ext)",
|
||||
"const x = ()=>{}, y = ()=>{x()}; y()",
|
||||
"const x = ext, y = ()=>{const x = ()=>{}; x()}; y()",
|
||||
// Identifier when mutated
|
||||
"const x = {}; x.y = ext",
|
||||
// IfStatement
|
||||
"let y;if (ext > 0) {y = 1} else {y = 2}",
|
||||
"if (false) {ext()}",
|
||||
"if (true) {} else {ext()}",
|
||||
// ImportDeclaration
|
||||
r#"import "import""#,
|
||||
r#"import x from "import-default""#,
|
||||
r#"import {x} from "import""#,
|
||||
r#"import {x as y} from "import""#,
|
||||
r#"import * as x from "import""#,
|
||||
r#"import /* tree-shaking no-side-effects-when-called */ x from "import-default-no-effects"; x()"#,
|
||||
r#"import /* test */ /*tree-shaking no-side-effects-when-called */ x from "import-default-no-effects"; x()"#,
|
||||
r#"import /* tree-shaking no-side-effects-when-called*/ /* test */ x from "import-default-no-effects"; x()"#,
|
||||
r#"import {/* tree-shaking no-side-effects-when-called */ x} from "import-no-effects"; x()"#,
|
||||
r#"import {x as /* tree-shaking no-side-effects-when-called */ y} from "import-no-effects"; y()"#,
|
||||
r#"import {x} from "import"; /*@__PURE__*/ x()"#,
|
||||
r#"import {x} from "import"; /* @__PURE__ */ x()"#,
|
||||
// JSXAttribute
|
||||
r#"class X {}; const x = <X test="3"/>"#,
|
||||
"class X {}; const x = <X test={3}/>",
|
||||
"class X {}; const x = <X test=<X/>/>",
|
||||
// JSXElement
|
||||
"class X {}; const x = <X/>",
|
||||
"class X {}; const x = <X>Text</X>",
|
||||
// JSXEmptyExpression
|
||||
"class X {}; const x = <X>{}</X>",
|
||||
// JSXExpressionContainer
|
||||
"class X {}; const x = <X>{3}</X>",
|
||||
// JSXIdentifier
|
||||
"class X {}; const x = <X/>",
|
||||
"const X = class {constructor() {this.x = 1}}; const x = <X/>",
|
||||
// JSXOpeningElement
|
||||
"class X {}; const x = <X/>",
|
||||
"class X {}; const x = <X></X>",
|
||||
r#"class X {}; const x = <X test="3"/>"#,
|
||||
// JSXSpreadAttribute
|
||||
"class X {}; const x = <X {...{x: 3}}/>",
|
||||
// LabeledStatement
|
||||
"loop: for(;true;){continue loop}",
|
||||
// Literal
|
||||
"const x = 3",
|
||||
"if (false) ext()",
|
||||
r#""use strict""#,
|
||||
// LogicalExpression
|
||||
"const x = 3 || 4",
|
||||
"true || ext()",
|
||||
"false && ext()",
|
||||
"if (false && false) ext()",
|
||||
"if (true && false) ext()",
|
||||
"if (false && true) ext()",
|
||||
"if (false || false) ext()",
|
||||
// MemberExpression
|
||||
"const x = ext.y",
|
||||
r#"const x = ext["y"]"#,
|
||||
"let x = ()=>{}; x.y = 1",
|
||||
// MemberExpression when called
|
||||
"const x = Object.keys({})",
|
||||
// MemberExpression when mutated
|
||||
"const x = {};x.y = ext",
|
||||
"const x = {y: 1};delete x.y",
|
||||
// MetaProperty
|
||||
"function x(){const y = new.target}; x()",
|
||||
// MethodDefinition
|
||||
"class x {a(){}}",
|
||||
"class x {static a(){}}",
|
||||
// NewExpression
|
||||
"const x = new (function (){this.x = 1})()",
|
||||
"function x(){this.y = 1}; const z = new x()",
|
||||
"/*@__PURE__*/ new ext()",
|
||||
// ObjectExpression
|
||||
"const x = {y: ext}",
|
||||
r#"const x = {["y"]: ext}"#,
|
||||
"const x = {};x.y = ext",
|
||||
// ObjectPattern
|
||||
"const {x} = {}",
|
||||
"const {[ext]: x} = {}",
|
||||
// RestElement
|
||||
"const [...x] = []",
|
||||
// ReturnStatement
|
||||
"(()=>{return})()",
|
||||
"(()=>{return 1})()",
|
||||
// SequenceExpression
|
||||
"let x = 1; x++, x++",
|
||||
"if (ext, false) ext()",
|
||||
// SwitchCase
|
||||
"switch(ext){case ext:const x = 1;break;default:}",
|
||||
// SwitchStatement
|
||||
"switch(ext){}",
|
||||
"const x = ()=>{}; switch(ext){case 1:const x = ext}; x()",
|
||||
"const x = ext; switch(ext){case 1:const x = ()=>{}; x()}",
|
||||
// TaggedTemplateExpression
|
||||
"const x = ()=>{}; const y = x``",
|
||||
// TemplateLiteral
|
||||
"const x = ``",
|
||||
"const x = `Literal`",
|
||||
"const x = `Literal ${ext}`",
|
||||
r#"const x = ()=>"a"; const y = `Literal ${x()}`"#,
|
||||
// ThisExpression
|
||||
"const y = this.x",
|
||||
// ThisExpression when mutated
|
||||
"const y = new (function (){this.x = 1})()",
|
||||
"const y = new (function (){{this.x = 1}})()",
|
||||
"const y = new (function (){(()=>{this.x = 1})()})()",
|
||||
"function x(){this.y = 1}; const y = new x()",
|
||||
// TryStatement
|
||||
"try {} catch (error) {}",
|
||||
"try {} finally {}",
|
||||
"try {} catch (error) {} finally {}",
|
||||
// UnaryExpression
|
||||
"!ext",
|
||||
"const x = {};delete x.y",
|
||||
r#"const x = {};delete x["y"]"#,
|
||||
// UpdateExpression
|
||||
"let x=1;x++",
|
||||
"const x = {};x.y++",
|
||||
// VariableDeclaration
|
||||
"const x = 1",
|
||||
// VariableDeclarator
|
||||
"var x, y",
|
||||
"var x = 1, y = 2",
|
||||
"const x = 1, y = 2",
|
||||
"let x = 1, y = 2",
|
||||
"const {x} = {}",
|
||||
// WhileStatement
|
||||
"while(true){}",
|
||||
"while(ext > 0){}",
|
||||
"const x = ()=>{}; while(true)x()",
|
||||
// YieldExpression
|
||||
"function* x(){const a = yield}; x()",
|
||||
"function* x(){yield ext}; x()",
|
||||
// Supports TypeScript nodes
|
||||
"interface Blub {}",
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// ArrayExpression
|
||||
"const x = [ext()]",
|
||||
"const x = [,,ext(),]",
|
||||
// ArrayPattern
|
||||
"const [x = ext()] = []",
|
||||
"const [,x = ext(),] = []",
|
||||
// ArrowFunctionExpression when called
|
||||
"(()=>{ext()})()",
|
||||
"(({a = ext()})=>{})()",
|
||||
"(a=>{a()})(ext)",
|
||||
"((...a)=>{a()})(ext)",
|
||||
"(({a})=>{a()})(ext)",
|
||||
"(a=>{a.x = 1})(ext)",
|
||||
"(a=>{const b = a;b.x = 1})(ext)",
|
||||
"((...a)=>{a.x = 1})(ext)",
|
||||
"(({a})=>{a.x = 1})(ext)",
|
||||
// AssignmentExpression
|
||||
"ext = 1",
|
||||
"ext += 1",
|
||||
"ext.x = 1",
|
||||
"const x = {};x[ext()] = 1",
|
||||
"this.x = 1",
|
||||
// AssignmentPattern
|
||||
"const {x = ext()} = {}",
|
||||
"const {y: {x = ext()} = {}} = {}",
|
||||
// AwaitExpression
|
||||
"const x = async ()=>{await ext()}; x()",
|
||||
// BinaryExpression
|
||||
"const x = 1 + ext()",
|
||||
"const x = ext() + 1",
|
||||
// BlockStatement
|
||||
"{ext()}",
|
||||
"var x=()=>{};{var x=ext}x()",
|
||||
"var x=ext;{x(); var x=()=>{}}",
|
||||
// CallExpression
|
||||
"(()=>{})(ext(), 1)",
|
||||
"(()=>{})(1, ext())",
|
||||
// CallExpression when called
|
||||
"const x = ()=>ext; const y = x(); y()",
|
||||
// CallExpression when mutated
|
||||
"const x = ()=>ext; const y = x(); y.z = 1",
|
||||
// CatchClause
|
||||
"try {} catch (error) {ext()}",
|
||||
"var x=()=>{}; try {} catch (error) {var x=ext}; x()",
|
||||
// ClassBody
|
||||
"class x {[ext()](){}}",
|
||||
// ClassBody when called
|
||||
"class x {constructor(){ext()}}; new x()",
|
||||
"class x {constructor(){ext()}}; const y = new x()",
|
||||
"class x extends ext {}; const y = new x()",
|
||||
"class y {constructor(){ext()}}; class x extends y {}; const z = new x()",
|
||||
"class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()",
|
||||
"class y{}; class x extends y{constructor(){super()}}; const z = new x()",
|
||||
// ClassDeclaration
|
||||
"class x extends ext() {}",
|
||||
"class x {[ext()](){}}",
|
||||
// ClassDeclaration when called
|
||||
"class x {constructor(){ext()}}; new x()",
|
||||
"class x {constructor(){ext()}}; const y = new x()",
|
||||
"class x extends ext {}; const y = new x()",
|
||||
// ClassExpression
|
||||
"const x = class extends ext() {}",
|
||||
"const x = class {[ext()](){}}",
|
||||
// ClassExpression when called
|
||||
"new (class {constructor(){ext()}})()",
|
||||
"const x = new (class {constructor(){ext()}})()",
|
||||
"const x = new (class extends ext {})()",
|
||||
// ClassProperty
|
||||
"class x {[ext()] = 1}",
|
||||
// ClassProperty when called
|
||||
"class x {y = ext()}; new x()",
|
||||
// ConditionalExpression
|
||||
"const x = ext() ? 1 : 2",
|
||||
"const x = ext ? ext() : 2",
|
||||
"const x = ext ? 1 : ext()",
|
||||
"if (false ? false : true) ext()",
|
||||
// ConditionalExpression when called
|
||||
"const x = ()=>{}; (true ? ext : x)()",
|
||||
"const x = ()=>{}; (false ? x : ext)()",
|
||||
"const x = ()=>{}; (ext ? x : ext)()",
|
||||
// DebuggerStatement
|
||||
"debugger",
|
||||
// DoWhileStatement
|
||||
"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",
|
||||
// ExportNamedDeclaration
|
||||
"export const x = ext()",
|
||||
"export const /* tree-shaking no-side-effects-when-called */ x = ext",
|
||||
"export function /* tree-shaking no-side-effects-when-called */ x(){ext()}",
|
||||
"const x = ext; export {/* tree-shaking no-side-effects-when-called */ x}",
|
||||
// ExpressionStatement
|
||||
"ext()",
|
||||
// ForInStatement
|
||||
"for(ext in {a: 1}){}",
|
||||
"for(const x in ext()){}",
|
||||
"for(const x in {a: 1}){ext()}",
|
||||
"for(const x in {a: 1}) ext()",
|
||||
// ForOfStatement
|
||||
"for(ext of {a: 1}){}",
|
||||
"for(const x of ext()){}",
|
||||
"for(const x of {a: 1}){ext()}",
|
||||
"for(const x of {a: 1}) ext()",
|
||||
// ForStatement
|
||||
"for(ext();;){}",
|
||||
"for(;ext();){}",
|
||||
"for(;true;ext()){}",
|
||||
"for(;true;) ext()",
|
||||
"for(;true;){ext()}",
|
||||
// FunctionDeclaration when called
|
||||
"function x(){ext()}; x()",
|
||||
"function x(){ext()}; const y = new x()",
|
||||
"function x(){ext()}; new x()",
|
||||
"function x(a = ext()){}; x()",
|
||||
"function x(a){a()}; x(ext)",
|
||||
"function x(...a){a()}; x(ext)",
|
||||
"function x({a}){a()}; x(ext)",
|
||||
"function x(a){a(); a(); a()}; x(ext)",
|
||||
"function x(a){a.y = 1}; x(ext)",
|
||||
"function x(...a){a.y = 1}; x(ext)",
|
||||
"function x({a}){a.y = 1}; x(ext)",
|
||||
"function x(a){a.y = 1; a.y = 2; a.y = 3}; x(ext)",
|
||||
"function x(){ext = 1}; x(); x(); x()",
|
||||
"function x(){ext = 1}; const y = new x(); y = new x(); y = new x()",
|
||||
// FunctionExpression when called
|
||||
"(function (){ext()}())",
|
||||
"const x = new (function (){ext()})()",
|
||||
"new (function (){ext()})()",
|
||||
"(function ({a = ext()}){}())",
|
||||
"(function (a){a()}(ext))",
|
||||
"(function (...a){a()}(ext))",
|
||||
"(function ({a}){a()}(ext))",
|
||||
"(function (a){a.x = 1}(ext))",
|
||||
"(function (a){const b = a;b.x = 1}(ext))",
|
||||
"(function (...a){a.x = 1}(ext))",
|
||||
"(function ({a}){a.x = 1}(ext))",
|
||||
// Identifier when called
|
||||
"ext()",
|
||||
"const x = ext; x()",
|
||||
"let x = ()=>{}; x = ext; x()",
|
||||
"var x = ()=>{}; var x = ext; x()",
|
||||
"const x = ()=>{ext()}; x()",
|
||||
"const x = ()=>{ext = 1}; x(); x(); x()",
|
||||
"let x = ()=>{}; const y = ()=>{x()}; x = ext; y()",
|
||||
"var x = ()=>{}; const y = ()=>{x()}; var x = ext; y()",
|
||||
"const x = ()=>{}; const {y} = x(); y()",
|
||||
"const x = ()=>{}; const [y] = x(); y()",
|
||||
// Identifier when mutated
|
||||
"var x = ext; x.y = 1",
|
||||
"var x = {}; x = ext; x.y = 1",
|
||||
"var x = {}; var x = ext; x.y = 1",
|
||||
"var x = {}; x = ext; x.y = 1; x.y = 1; x.y = 1",
|
||||
"const x = {y:ext}; const {y} = x; y.z = 1",
|
||||
// IfStatement
|
||||
"if (ext()>0){}",
|
||||
"if (1>0){ext()}",
|
||||
"if (1<0){} else {ext()}",
|
||||
"if (ext>0){ext()} else {ext()}",
|
||||
// ImportDeclaration
|
||||
r#"import x from "import-default"; x()"#,
|
||||
r#"import x from "import-default"; x.z = 1"#,
|
||||
r#"import {x} from "import"; x()"#,
|
||||
r#"import {x} from "import"; x.z = 1"#,
|
||||
r#"import {x as y} from "import"; y()"#,
|
||||
r#"import {x as y} from "import"; y.a = 1"#,
|
||||
r#"import * as y from "import"; y.x()"#,
|
||||
r#"import * as y from "import"; y.x = 1"#,
|
||||
// JSXAttribute
|
||||
"class X {}; const x = <X test={ext()}/>",
|
||||
"class X {}; class Y {constructor(){ext()}}; const x = <X test=<Y/>/>",
|
||||
// JSXElement
|
||||
"class X {constructor(){ext()}}; const x = <X/>",
|
||||
"class X {}; const x = <X>{ext()}</X>",
|
||||
// JSXExpressionContainer
|
||||
"class X {}; const x = <X>{ext()}</X>",
|
||||
// JSXIdentifier
|
||||
"class X {constructor(){ext()}}; const x = <X/>",
|
||||
"const X = class {constructor(){ext()}}; const x = <X/>",
|
||||
"const x = <Ext/>",
|
||||
// JSXMemberExpression
|
||||
"const X = {Y: ext}; const x = <X.Y />",
|
||||
// JSXOpeningElement
|
||||
"class X {}; const x = <X test={ext()}/>",
|
||||
// JSXSpreadAttribute
|
||||
"class X {}; const x = <X {...{x: ext()}}/>",
|
||||
// LabeledStatement
|
||||
"loop: for(;true;){ext()}",
|
||||
// Literal
|
||||
"if (true) ext()",
|
||||
// LogicalExpression
|
||||
"ext() && true",
|
||||
"true && ext()",
|
||||
"false || ext()",
|
||||
"if (true && true) ext()",
|
||||
"if (false || true) ext()",
|
||||
"if (true || false) ext()",
|
||||
"if (true || true) ext()",
|
||||
// MemberExpression
|
||||
"const x = {};const y = x[ext()]",
|
||||
// MemberExpression when called
|
||||
"ext.x()",
|
||||
"const x = {}; x.y()",
|
||||
"const x = ()=>{}; x().y()",
|
||||
"const Object = {}; const x = Object.keys({})",
|
||||
"const x = {}; x[ext()]()",
|
||||
// MemberExpression when mutated
|
||||
"const x = {y: ext};x.y.z = 1",
|
||||
"const x = {y:ext};const y = x.y; y.z = 1",
|
||||
"const x = {y: ext};delete x.y.z",
|
||||
// MethodDefinition
|
||||
"class x {static [ext()](){}}",
|
||||
// NewExpression
|
||||
"const x = new ext()",
|
||||
"new ext()",
|
||||
// ObjectExpression
|
||||
"const x = {y: ext()}",
|
||||
r#"const x = {["y"]: ext()}"#,
|
||||
"const x = {[ext()]: 1}",
|
||||
// ObjectPattern
|
||||
"const {[ext()]: x} = {}",
|
||||
// ReturnStatement
|
||||
"(()=>{return ext()})()",
|
||||
// SequenceExpression
|
||||
"ext(), 1",
|
||||
"1, ext()",
|
||||
"if (1, true) ext()",
|
||||
"if (1, ext) ext()",
|
||||
// Super when called
|
||||
"class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()",
|
||||
"class y{}; class x extends y{constructor(){super(); super.test()}}; const z = new x()",
|
||||
"class y{}; class x extends y{constructor(){super()}}; const z = new x()",
|
||||
// SwitchCase
|
||||
"switch(ext){case ext():}",
|
||||
"switch(ext){case 1:ext()}",
|
||||
// SwitchStatement
|
||||
"switch(ext()){}",
|
||||
"var x=()=>{}; switch(ext){case 1:var x=ext}; x()",
|
||||
// TaggedTemplateExpression
|
||||
"const x = ext``",
|
||||
"ext``",
|
||||
"const x = ()=>{}; const y = x`${ext()}`",
|
||||
// TemplateLiteral
|
||||
"const x = `Literal ${ext()}`",
|
||||
// ThisExpression when mutated
|
||||
"this.x = 1",
|
||||
"(()=>{this.x = 1})()",
|
||||
"(function(){this.x = 1}())",
|
||||
"const y = new (function (){(function(){this.x = 1}())})()",
|
||||
"function x(){this.y = 1}; x()",
|
||||
// ThrowStatement
|
||||
r#"throw new Error("Hello Error")"#,
|
||||
// TryStatement
|
||||
"try {ext()} catch (error) {}",
|
||||
"try {} finally {ext()}",
|
||||
// UnaryExpression
|
||||
"!ext()",
|
||||
"delete ext.x",
|
||||
r#"delete ext["x"]"#,
|
||||
"const x = ()=>{};delete x()",
|
||||
// UpdateExpression
|
||||
"ext++",
|
||||
"const x = {};x[ext()]++",
|
||||
// VariableDeclaration
|
||||
"const x = ext()",
|
||||
// VariableDeclarator
|
||||
"var x = ext(),y = ext()",
|
||||
"const x = ext(),y = ext()",
|
||||
"let x = ext(),y = ext()",
|
||||
"const {x = ext()} = {}",
|
||||
// WhileStatement
|
||||
"while(ext()){}",
|
||||
"while(true)ext()",
|
||||
"while(true){ext()}",
|
||||
// YieldExpression
|
||||
"function* x(){yield ext()}; x()",
|
||||
// YieldExpression when called
|
||||
"function* x(){yield ext()}; x()"
|
||||
];
|
||||
|
||||
Tester::new(NoSideEffectsInInitialization::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
3
justfile
3
justfile
|
|
@ -117,6 +117,9 @@ new-react-perf-rule name:
|
|||
new-n-rule name:
|
||||
cargo run -p rulegen {{name}} n
|
||||
|
||||
new-tree-shaking-rule name:
|
||||
cargo run -p rulegen {{name}} tree-shaking
|
||||
|
||||
# Upgrade all Rust dependencies
|
||||
upgrade:
|
||||
cargo upgrade --incompatible
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self},
|
||||
fmt::{Display, Formatter},
|
||||
collections::HashMap,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
|
|
@ -16,7 +16,7 @@ use oxc_ast::{
|
|||
Visit,
|
||||
};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_span::{GetSpan, SourceType};
|
||||
use oxc_span::{GetSpan, SourceType, Span};
|
||||
use serde::Serialize;
|
||||
use ureq::Response;
|
||||
|
||||
|
|
@ -53,21 +53,35 @@ const REACT_PERF_TEST_PATH: &str =
|
|||
const NODE_TEST_PATH: &str =
|
||||
"https://raw.githubusercontent.com/eslint-community/eslint-plugin-n/master/tests/lib/rules";
|
||||
|
||||
const TREE_SHAKING_PATH: &str =
|
||||
"https://raw.githubusercontent.com/lukastaegert/eslint-plugin-tree-shaking/master/src/rules";
|
||||
|
||||
struct TestCase<'a> {
|
||||
source_text: String,
|
||||
code: Option<String>,
|
||||
group_comment: Option<String>,
|
||||
config: Option<Cow<'a, str>>,
|
||||
settings: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> TestCase<'a> {
|
||||
fn new(source_text: &str, arg: &'a Expression<'a>) -> Self {
|
||||
let mut test_case =
|
||||
Self { source_text: source_text.to_string(), code: None, config: None, settings: None };
|
||||
let mut test_case = Self {
|
||||
source_text: source_text.to_string(),
|
||||
code: None,
|
||||
config: None,
|
||||
settings: None,
|
||||
group_comment: None,
|
||||
};
|
||||
test_case.visit_expression(arg);
|
||||
test_case
|
||||
}
|
||||
|
||||
fn with_group_comment(mut self, comment: String) -> Self {
|
||||
self.group_comment = Some(comment);
|
||||
self
|
||||
}
|
||||
|
||||
fn code(&self, need_config: bool, need_settings: bool) -> String {
|
||||
self.code
|
||||
.as_ref()
|
||||
|
|
@ -106,6 +120,10 @@ impl<'a> TestCase<'a> {
|
|||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn group_comment(&self) -> Option<&str> {
|
||||
self.group_comment.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for TestCase<'a> {
|
||||
|
|
@ -271,23 +289,56 @@ struct State<'a> {
|
|||
source_text: &'a str,
|
||||
valid_tests: Vec<&'a Expression<'a>>,
|
||||
invalid_tests: Vec<&'a Expression<'a>>,
|
||||
expression_to_group_comment_map: HashMap<Span, String>,
|
||||
group_comment_stack: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
fn new(source_text: &'a str) -> Self {
|
||||
Self { source_text, valid_tests: vec![], invalid_tests: vec![] }
|
||||
Self {
|
||||
source_text,
|
||||
valid_tests: vec![],
|
||||
invalid_tests: vec![],
|
||||
expression_to_group_comment_map: HashMap::new(),
|
||||
group_comment_stack: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn pass_cases(&self) -> Vec<TestCase> {
|
||||
self.valid_tests.iter().map(|arg| TestCase::new(self.source_text, arg)).collect::<Vec<_>>()
|
||||
self.get_test_cases(&self.valid_tests)
|
||||
}
|
||||
|
||||
fn fail_cases(&self) -> Vec<TestCase> {
|
||||
self.invalid_tests
|
||||
self.get_test_cases(&self.invalid_tests)
|
||||
}
|
||||
|
||||
fn get_test_cases(&self, tests: &[&'a Expression<'a>]) -> Vec<TestCase> {
|
||||
tests
|
||||
.iter()
|
||||
.map(|arg| TestCase::new(self.source_text, arg))
|
||||
.map(|arg| {
|
||||
let case = TestCase::new(self.source_text, arg);
|
||||
if let Some(group_comment) = self.expression_to_group_comment_map.get(&arg.span()) {
|
||||
case.with_group_comment(group_comment.to_string())
|
||||
} else {
|
||||
case
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn get_comment(&self) -> String {
|
||||
self.group_comment_stack.join(" ")
|
||||
}
|
||||
|
||||
fn add_valid_test(&mut self, expr: &'a Expression<'a>) {
|
||||
self.valid_tests.push(expr);
|
||||
self.expression_to_group_comment_map.insert(expr.span(), self.get_comment());
|
||||
}
|
||||
|
||||
fn add_invalid_test(&mut self, expr: &'a Expression<'a>) {
|
||||
self.invalid_tests.push(expr);
|
||||
self.expression_to_group_comment_map.insert(expr.span(), self.get_comment());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for State<'a> {
|
||||
|
|
@ -319,21 +370,29 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
self.visit_expression(&stmt.expression);
|
||||
}
|
||||
|
||||
fn visit_expression(&mut self, expr: &Expression<'a>) {
|
||||
if let Expression::CallExpression(call_expr) = expr {
|
||||
for arg in &call_expr.arguments {
|
||||
self.visit_argument(arg);
|
||||
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
|
||||
let mut pushed = false;
|
||||
if let Expression::Identifier(ident) = &expr.callee {
|
||||
// Add describe's first parameter as part group comment
|
||||
// e.g. for `describe('valid', () => { ... })`, the group comment will be "valid"
|
||||
if ident.name == "describe" {
|
||||
if let Some(Argument::Expression(Expression::StringLiteral(lit))) =
|
||||
expr.arguments.first()
|
||||
{
|
||||
pushed = true;
|
||||
self.group_comment_stack.push(lit.value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for arg in &expr.arguments {
|
||||
self.visit_argument(arg);
|
||||
}
|
||||
|
||||
fn visit_argument(&mut self, arg: &Argument<'a>) {
|
||||
if let Argument::Expression(Expression::ObjectExpression(obj_expr)) = arg {
|
||||
for obj_prop in &obj_expr.properties {
|
||||
let ObjectPropertyKind::ObjectProperty(prop) = obj_prop else { return };
|
||||
self.visit_object_property(prop);
|
||||
}
|
||||
if pushed {
|
||||
self.group_comment_stack.pop();
|
||||
}
|
||||
|
||||
self.visit_expression(&expr.callee);
|
||||
}
|
||||
|
||||
fn visit_object_property(&mut self, prop: &ObjectProperty<'a>) {
|
||||
|
|
@ -344,7 +403,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
let array_expr = self.alloc(array_expr);
|
||||
for arg in &array_expr.elements {
|
||||
if let ArrayExpressionElement::Expression(expr) = arg {
|
||||
self.valid_tests.push(expr);
|
||||
self.add_valid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -354,7 +413,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
{
|
||||
for arg in args {
|
||||
if let Argument::Expression(expr) = arg {
|
||||
self.valid_tests.push(expr);
|
||||
self.add_valid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -368,7 +427,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
let array_expr = self.alloc(array_expr);
|
||||
for arg in &array_expr.elements {
|
||||
if let ArrayExpressionElement::Expression(expr) = arg {
|
||||
self.valid_tests.push(expr);
|
||||
self.add_valid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -380,7 +439,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
let array_expr = self.alloc(array_expr);
|
||||
for arg in &array_expr.elements {
|
||||
if let ArrayExpressionElement::Expression(expr) = arg {
|
||||
self.invalid_tests.push(expr);
|
||||
self.add_invalid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -390,7 +449,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
{
|
||||
for arg in args {
|
||||
if let Argument::Expression(expr) = arg {
|
||||
self.invalid_tests.push(expr);
|
||||
self.add_invalid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -404,7 +463,7 @@ impl<'a> Visit<'a> for State<'a> {
|
|||
let array_expr = self.alloc(array_expr);
|
||||
for arg in &array_expr.elements {
|
||||
if let ArrayExpressionElement::Expression(expr) = arg {
|
||||
self.invalid_tests.push(expr);
|
||||
self.add_invalid_test(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,6 +517,7 @@ pub enum RuleKind {
|
|||
NextJS,
|
||||
JSDoc,
|
||||
Node,
|
||||
TreeShaking,
|
||||
}
|
||||
|
||||
impl RuleKind {
|
||||
|
|
@ -474,6 +534,7 @@ impl RuleKind {
|
|||
"nextjs" => Self::NextJS,
|
||||
"jsdoc" => Self::JSDoc,
|
||||
"n" => Self::Node,
|
||||
"tree-shaking" => Self::TreeShaking,
|
||||
_ => Self::ESLint,
|
||||
}
|
||||
}
|
||||
|
|
@ -494,6 +555,7 @@ impl Display for RuleKind {
|
|||
Self::NextJS => write!(f, "eslint-plugin-next"),
|
||||
Self::JSDoc => write!(f, "eslint-plugin-jsdoc"),
|
||||
Self::Node => write!(f, "eslint-plugin-n"),
|
||||
Self::TreeShaking => write!(f, "eslint-plugin-tree-shaking"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -519,6 +581,7 @@ fn main() {
|
|||
RuleKind::NextJS => format!("{NEXT_JS_TEST_PATH}/{kebab_rule_name}.test.ts"),
|
||||
RuleKind::JSDoc => format!("{JSDOC_TEST_PATH}/{camel_rule_name}.js"),
|
||||
RuleKind::Node => format!("{NODE_TEST_PATH}/{kebab_rule_name}.js"),
|
||||
RuleKind::TreeShaking => format!("{TREE_SHAKING_PATH}/{kebab_rule_name}.test.ts"),
|
||||
RuleKind::Oxc | RuleKind::DeepScan => String::new(),
|
||||
};
|
||||
|
||||
|
|
@ -552,19 +615,34 @@ fn main() {
|
|||
let fail_has_settings = fail_cases.iter().any(|case| case.settings.is_some());
|
||||
let has_settings = pass_has_settings || fail_has_settings;
|
||||
|
||||
let pass_cases = pass_cases
|
||||
.into_iter()
|
||||
.map(|c| c.code(has_config, has_settings))
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",\n");
|
||||
let gen_cases_string = |cases: Vec<TestCase>| {
|
||||
let mut codes = vec![];
|
||||
let mut last_comment = String::new();
|
||||
for case in cases {
|
||||
let current_comment = case.group_comment();
|
||||
let mut code = case.code(has_config, has_settings);
|
||||
if code.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(current_comment) = current_comment {
|
||||
if current_comment != last_comment {
|
||||
last_comment = current_comment.to_string();
|
||||
code = format!(
|
||||
"// {}\n{}",
|
||||
&last_comment,
|
||||
case.code(has_config, has_settings)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let fail_cases = fail_cases
|
||||
.into_iter()
|
||||
.map(|c| c.code(has_config, has_settings))
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",\n");
|
||||
codes.push(code);
|
||||
}
|
||||
|
||||
codes.join(",\n")
|
||||
};
|
||||
|
||||
let pass_cases = gen_cases_string(pass_cases);
|
||||
let fail_cases = gen_cases_string(fail_cases);
|
||||
|
||||
Context::new(plugin_name, &rule_name, pass_cases, fail_cases)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ impl<'a> Template<'a> {
|
|||
RuleKind::NextJS => Path::new("crates/oxc_linter/src/rules/nextjs"),
|
||||
RuleKind::JSDoc => Path::new("crates/oxc_linter/src/rules/jsdoc"),
|
||||
RuleKind::Node => Path::new("crates/oxc_linter/src/rules/node"),
|
||||
RuleKind::TreeShaking => Path::new("crates/oxc_linter/src/rules/tree_shaking"),
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(path)?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue