diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index f5b784f6b9ce..e8fd84a2a9d9 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -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, } diff --git a/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization.rs b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization.rs new file mode 100644 index 000000000000..eb20261ddd0c --- /dev/null +++ b/crates/oxc_linter/src/rules/tree_shaking/no_side_effects_in_initialization.rs @@ -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); + +/// +#[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 + r"[]", + r"const x = []", + r"const x = [ext,ext]", + r"const x = [1,,2,]", + // ArrayPattern + r"const [x] = []", + r"const [,x,] = []", + // ArrowFunctionExpression + r"const x = a=>{a(); ext()}", + // ArrowFunctionExpression when called + r"(()=>{})()", + r"(a=>{})()", + r"((...a)=>{})()", + r"(({a})=>{})()", + // ArrowFunctionExpression when mutated + r"const x = ()=>{}; x.y = 1", + // AssignmentExpression + r"var x;x = {}", + r"var x;x += 1", + r"const x = {}; x.y = 1", + r#"const x = {}; x["y"] = 1"#, + r"function x(){this.y = 1}; const z = new x()", + r"let x = 1; x = 2 + 3", + r"let x; x = 2 + 3", + // AssignmentPattern + r"const {x = ext} = {}", + r"const {x: y = ext} = {}", + r"const {[ext]: x = ext} = {}", + r"const x = ()=>{}, {y = x()} = {}", + // BinaryExpression + r"const x = 1 + 2", + r"if (1-1) ext()", + // BlockStatement + r"{}", + r"const x = ()=>{};{const x = ext}x()", + r"const x = ext;{const x = ()=>{}; x()}", + // BreakStatement + r"while(true){break}", + // CallExpression + r"(a=>{const y = a})(ext, ext)", + r"const x = ()=>{}, y = ()=>{}; x(y())", + // CatchClause + r"try {} catch (error) {}", + r"const x = ()=>{}; try {} catch (error) {const x = ext}; x()", + r"const x = ext; try {} catch (error) {const x = ()=>{}; x()}", + // ClassBody + r"class x {a(){ext()}}", + // ClassBody when called + r"class x {a(){ext()}}; const y = new x()", + r"class x {constructor(){}}; const y = new x()", + r"class y{}; class x extends y{}; const z = new x()", + // ClassDeclaration + r"class x extends ext {}", + // ClassDeclaration when called + r"class x {}; const y = new x()", + // ClassExpression + r"const x = class extends ext {}", + // ClassExpression when called + r"const x = new (class {})()", + // ClassProperty + r"class x {y}", + r"class x {y = 1}", + r"class x {y = ext()}", + // ConditionalExpression + r"const x = ext ? 1 : 2", + r"const x = true ? 1 : ext()", + r"const x = false ? ext() : 2", + r"if (true ? false : true) ext()", + r"ext ? 1 : ext.x", + r"ext ? ext.x : 1", + // ConditionalExpression when called + r"const x = ()=>{}, y = ()=>{};(ext ? x : y)()", + r"const x = ()=>{}; (true ? x : ext)()", + r"const x = ()=>{}; (false ? ext : x)()", + // ContinueStatement + r"while(true){continue}", + // DoWhileStatement + r"do {} while(true)", + r"do {} while(ext > 0)", + r"const x = ()=>{}; do x(); while(true)", + // EmptyStatement + r";", + // ExportAllDeclaration + r#"export * from \"import\""#, + // ExportDefaultDeclaration + r"export default ext", + r"const x = ext; export default x", + r"export default function(){}", + r"export default (function(){})", + r"const x = function(){}; export default /* tree-shaking no-side-effects-when-called */ x", + r"export default /* tree-shaking no-side-effects-when-called */ function(){}", + // ExportNamedDeclaration + r"export const x = ext", + r"export function x(){ext()}", + r"const x = ext; export {x}", + r#"export {x} from \"import\""#, + r#"export {x as y} from \"import\""#, + r#"export {x as default} from \"import\""#, + r"export const /* tree-shaking no-side-effects-when-called */ x = function(){}", + r"export function /* tree-shaking no-side-effects-when-called */ x(){}", + r"const x = function(){}; export {/* tree-shaking no-side-effects-when-called */ x}", + // ExpressionStatement + r"const x = 1", + // ForInStatement + r"for(const x in ext){x = 1}", + r"let x; for(x in ext){}", + // ForStatement + r"for(let i = 0; i < 3; i++){i++}", + r"for(;;){}", + // FunctionDeclaration + r"function x(a){a(); ext()}", + // FunctionDeclaration when called + r"function x(){}; x()", + r"function x(a){}; x()", + r"function x(...a){}; x()", + r"function x({a}){}; x()", + // FunctionDeclaration when mutated + r"function x(){}; x.y = 1", + // FunctionExpression + r"const x = function (a){a(); ext()}", + // FunctionExpression when called + r"(function (){}())", + r"(function (a){}())", + r"(function (...a){}())", + r"(function ({a}){}())", + // Identifier + r"var x;x = 1", + // Identifier when called + r"const x = ()=>{};x(ext)", + r"function x(){};x(ext)", + r"var x = ()=>{};x(ext)", + r"const x = ()=>{}, y = ()=>{x()}; y()", + r"const x = ext, y = ()=>{const x = ()=>{}; x()}; y()", + // Identifier when mutated + r"const x = {}; x.y = ext", + // IfStatement + r"let y;if (ext > 0) {y = 1} else {y = 2}", + r"if (false) {ext()}", + r"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 = "#, + r"class X {}; const x = ", + r"class X {}; const x = />", + // JSXElement + r"class X {}; const x = ", + r"class X {}; const x = Text", + // JSXEmptyExpression + r"class X {}; const x = {}", + // JSXExpressionContainer + r"class X {}; const x = {3}", + // JSXIdentifier + r"class X {}; const x = ", + r"const X = class {constructor() {this.x = 1}}; const x = ", + // JSXOpeningElement + r"class X {}; const x = ", + r"class X {}; const x = ", + r#"class X {}; const x = "#, + // JSXSpreadAttribute + r"class X {}; const x = ", + // LabeledStatement + r"loop: for(;true;){continue loop}", + // Literal + r"const x = 3", + r"if (false) ext()", + r#"\"use strict\""#, + // LogicalExpression + r"const x = 3 || 4", + r"true || ext()", + r"false && ext()", + r"if (false && false) ext()", + r"if (true && false) ext()", + r"if (false && true) ext()", + r"if (false || false) ext()", + // MemberExpression + r"const x = ext.y", + r#"const x = ext[\"y\"]"#, + r"let x = ()=>{}; x.y = 1", + // MemberExpression when called + r"const x = Object.keys({})", + // MemberExpression when mutated + r"const x = {};x.y = ext", + r"const x = {y: 1};delete x.y", + // MetaProperty + r"function x(){const y = new.target}; x()", + // MethodDefinition + r"class x {a(){}}", + r"class x {static a(){}}", + // NewExpression + r"const x = new (function (){this.x = 1})()", + r"function x(){this.y = 1}; const z = new x()", + r"/*@__PURE__*/ new ext()", + // ObjectExpression + r"const x = {y: ext}", + r#"const x = {[\"y\"]: ext}"#, + r"const x = {};x.y = ext", + // ObjectPattern + r"const {x} = {}", + r"const {[ext]: x} = {}", + // RestElement + r"const [...x] = []", + // ReturnStatement + r"(()=>{return})()", + r"(()=>{return 1})()", + // SequenceExpression + r"let x = 1; x++, x++", + r"if (ext, false) ext()", + // SwitchCase + r"switch(ext){case ext:const x = 1;break;default:}", + // SwitchStatement + r"switch(ext){}", + r"const x = ()=>{}; switch(ext){case 1:const x = ext}; x()", + r"const x = ext; switch(ext){case 1:const x = ()=>{}; x()}", + // TaggedTemplateExpression + r"const x = ()=>{}; const y = x``", + // TemplateLiteral + r"const x = ``", + r"const x = `Literal`", + r"const x = `Literal ${ext}`", + r#"const x = ()=>\"a\"; const y = `Literal ${x()}`"#, + // ThisExpression + r"const y = this.x", + // ThisExpression when mutated + r"const y = new (function (){this.x = 1})()", + r"const y = new (function (){{this.x = 1}})()", + r"const y = new (function (){(()=>{this.x = 1})()})()", + r"function x(){this.y = 1}; const y = new x()", + // TryStatement + r"try {} catch (error) {}", + r"try {} finally {}", + r"try {} catch (error) {} finally {}", + // UnaryExpression + r"!ext", + r"const x = {};delete x.y", + r#"const x = {};delete x[\"y\"]"#, + // UpdateExpression + r"let x=1;x++", + r"const x = {};x.y++", + // VariableDeclaration + r"const x = 1", + // VariableDeclarator + r"var x, y", + r"var x = 1, y = 2", + r"const x = 1, y = 2", + r"let x = 1, y = 2", + r"const {x} = {}", + // WhileStatement + r"while(true){}", + r"while(ext > 0){}", + r"const x = ()=>{}; while(true)x()", + // YieldExpression + r"function* x(){const a = yield}; x()", + r"function* x(){yield ext}; x()", + // Supports TypeScript nodes + r"interface Blub {}", + ]; + + let fail = vec![ + // ArrayExpression + r"const x = [ext()]", + r"const x = [,,ext(),]", + // ArrayPattern + r"const [x = ext()] = []", + r"const [,x = ext(),] = []", + // ArrowFunctionExpression when called + r"(()=>{ext()})()", + r"(({a = ext()})=>{})()", + r"(a=>{a()})(ext)", + r"((...a)=>{a()})(ext)", + r"(({a})=>{a()})(ext)", + r"(a=>{a.x = 1})(ext)", + r"(a=>{const b = a;b.x = 1})(ext)", + r"((...a)=>{a.x = 1})(ext)", + r"(({a})=>{a.x = 1})(ext)", + // AssignmentExpression + r"ext = 1", + r"ext += 1", + r"ext.x = 1", + r"const x = {};x[ext()] = 1", + r"this.x = 1", + // AssignmentPattern + r"const {x = ext()} = {}", + r"const {y: {x = ext()} = {}} = {}", + // AwaitExpression + r"const x = async ()=>{await ext()}; x()", + // BinaryExpression + r"const x = 1 + ext()", + r"const x = ext() + 1", + // BlockStatement + r"{ext()}", + r"var x=()=>{};{var x=ext}x()", + r"var x=ext;{x(); var x=()=>{}}", + // CallExpression + r"(()=>{})(ext(), 1)", + r"(()=>{})(1, ext())", + // CallExpression when called + r"const x = ()=>ext; const y = x(); y()", + // CallExpression when mutated + r"const x = ()=>ext; const y = x(); y.z = 1", + // CatchClause + r"try {} catch (error) {ext()}", + r"var x=()=>{}; try {} catch (error) {var x=ext}; x()", + // ClassBody + r"class x {[ext()](){}}", + // ClassBody when called + r"class x {constructor(){ext()}}; new x()", + r"class x {constructor(){ext()}}; const y = new x()", + r"class x extends ext {}; const y = new x()", + r"class y {constructor(){ext()}}; class x extends y {}; const z = new x()", + r"class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()", + r"class y{}; class x extends y{constructor(){super()}}; const z = new x()", + // ClassDeclaration + r"class x extends ext() {}", + r"class x {[ext()](){}}", + // ClassDeclaration when called + r"class x {constructor(){ext()}}; new x()", + r"class x {constructor(){ext()}}; const y = new x()", + r"class x extends ext {}; const y = new x()", + // ClassExpression + r"const x = class extends ext() {}", + r"const x = class {[ext()](){}}", + // ClassExpression when called + r"new (class {constructor(){ext()}})()", + r"const x = new (class {constructor(){ext()}})()", + r"const x = new (class extends ext {})()", + // ClassProperty + r"class x {[ext()] = 1}", + // ClassProperty when called + r"class x {y = ext()}; new x()", + // ConditionalExpression + r"const x = ext() ? 1 : 2", + r"const x = ext ? ext() : 2", + r"const x = ext ? 1 : ext()", + r"if (false ? false : true) ext()", + // ConditionalExpression when called + r"const x = ()=>{}; (true ? ext : x)()", + r"const x = ()=>{}; (false ? x : ext)()", + r"const x = ()=>{}; (ext ? x : ext)()", + // DebuggerStatement + r"debugger", + // DoWhileStatement + r"do {} while(ext())", + r"do ext(); while(true)", + r"do {ext()} while(true)", + // ExportDefaultDeclaration + r"export default ext()", + r"export default /* tree-shaking no-side-effects-when-called */ ext", + r"const x = ext; export default /* tree-shaking no-side-effects-when-called */ x", + // ExportNamedDeclaration + r"export const x = ext()", + r"export const /* tree-shaking no-side-effects-when-called */ x = ext", + r"export function /* tree-shaking no-side-effects-when-called */ x(){ext()}", + r"const x = ext; export {/* tree-shaking no-side-effects-when-called */ x}", + // ExpressionStatement + r"ext()", + // ForInStatement + r"for(ext in {a: 1}){}", + r"for(const x in ext()){}", + r"for(const x in {a: 1}){ext()}", + r"for(const x in {a: 1}) ext()", + // ForOfStatement + r"for(ext of {a: 1}){}", + r"for(const x of ext()){}", + r"for(const x of {a: 1}){ext()}", + r"for(const x of {a: 1}) ext()", + // ForStatement + r"for(ext();;){}", + r"for(;ext();){}", + r"for(;true;ext()){}", + r"for(;true;) ext()", + r"for(;true;){ext()}", + // FunctionDeclaration when called + r"function x(){ext()}; x()", + r"function x(){ext()}; const y = new x()", + r"function x(){ext()}; new x()", + r"function x(a = ext()){}; x()", + r"function x(a){a()}; x(ext)", + r"function x(...a){a()}; x(ext)", + r"function x({a}){a()}; x(ext)", + r"function x(a){a(); a(); a()}; x(ext)", + r"function x(a){a.y = 1}; x(ext)", + r"function x(...a){a.y = 1}; x(ext)", + r"function x({a}){a.y = 1}; x(ext)", + r"function x(a){a.y = 1; a.y = 2; a.y = 3}; x(ext)", + r"function x(){ext = 1}; x(); x(); x()", + r"function x(){ext = 1}; const y = new x(); y = new x(); y = new x()", + // FunctionExpression when called + r"(function (){ext()}())", + r"const x = new (function (){ext()})()", + r"new (function (){ext()})()", + r"(function ({a = ext()}){}())", + r"(function (a){a()}(ext))", + r"(function (...a){a()}(ext))", + r"(function ({a}){a()}(ext))", + r"(function (a){a.x = 1}(ext))", + r"(function (a){const b = a;b.x = 1}(ext))", + r"(function (...a){a.x = 1}(ext))", + r"(function ({a}){a.x = 1}(ext))", + // Identifier when called + r"ext()", + r"const x = ext; x()", + r"let x = ()=>{}; x = ext; x()", + r"var x = ()=>{}; var x = ext; x()", + r"const x = ()=>{ext()}; x()", + r"const x = ()=>{ext = 1}; x(); x(); x()", + r"let x = ()=>{}; const y = ()=>{x()}; x = ext; y()", + r"var x = ()=>{}; const y = ()=>{x()}; var x = ext; y()", + r"const x = ()=>{}; const {y} = x(); y()", + r"const x = ()=>{}; const [y] = x(); y()", + // Identifier when mutated + r"var x = ext; x.y = 1", + r"var x = {}; x = ext; x.y = 1", + r"var x = {}; var x = ext; x.y = 1", + r"var x = {}; x = ext; x.y = 1; x.y = 1; x.y = 1", + r"const x = {y:ext}; const {y} = x; y.z = 1", + // IfStatement + r"if (ext()>0){}", + r"if (1>0){ext()}", + r"if (1<0){} else {ext()}", + r"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 + r"class X {}; const x = ", + r"class X {}; class Y {constructor(){ext()}}; const x = />", + // JSXElement + r"class X {constructor(){ext()}}; const x = ", + r"class X {}; const x = {ext()}", + // JSXExpressionContainer + r"class X {}; const x = {ext()}", + // JSXIdentifier + r"class X {constructor(){ext()}}; const x = ", + r"const X = class {constructor(){ext()}}; const x = ", + r"const x = ", + // JSXMemberExpression + r"const X = {Y: ext}; const x = ", + // JSXOpeningElement + r"class X {}; const x = ", + // JSXSpreadAttribute + r"class X {}; const x = ", + // LabeledStatement + r"loop: for(;true;){ext()}", + // Literal + r"if (true) ext()", + // LogicalExpression + r"ext() && true", + r"true && ext()", + r"false || ext()", + r"if (true && true) ext()", + r"if (false || true) ext()", + r"if (true || false) ext()", + r"if (true || true) ext()", + // MemberExpression + r"const x = {};const y = x[ext()]", + // MemberExpression when called + r"ext.x()", + r"const x = {}; x.y()", + r"const x = ()=>{}; x().y()", + r"const Object = {}; const x = Object.keys({})", + r"const x = {}; x[ext()]()", + // MemberExpression when mutated + r"const x = {y: ext};x.y.z = 1", + r"const x = {y:ext};const y = x.y; y.z = 1", + r"const x = {y: ext};delete x.y.z", + // MethodDefinition + r"class x {static [ext()](){}}", + // NewExpression + r"const x = new ext()", + r"new ext()", + // ObjectExpression + r"const x = {y: ext()}", + r#"const x = {[\"y\"]: ext()}"#, + r"const x = {[ext()]: 1}", + // ObjectPattern + r"const {[ext()]: x} = {}", + // ReturnStatement + r"(()=>{return ext()})()", + // SequenceExpression + r"ext(), 1", + r"1, ext()", + r"if (1, true) ext()", + r"if (1, ext) ext()", + // Super when called + r"class y {constructor(){ext()}}; class x extends y {constructor(){super()}}; const z = new x()", + r"class y{}; class x extends y{constructor(){super(); super.test()}}; const z = new x()", + r"class y{}; class x extends y{constructor(){super()}}; const z = new x()", + // SwitchCase + r"switch(ext){case ext():}", + r"switch(ext){case 1:ext()}", + // SwitchStatement + r"switch(ext()){}", + r"var x=()=>{}; switch(ext){case 1:var x=ext}; x()", + // TaggedTemplateExpression + r"const x = ext``", + r"ext``", + r"const x = ()=>{}; const y = x`${ext()}`", + // TemplateLiteral + r"const x = `Literal ${ext()}`", + // ThisExpression when mutated + r"this.x = 1", + r"(()=>{this.x = 1})()", + r"(function(){this.x = 1}())", + r"const y = new (function (){(function(){this.x = 1}())})()", + r"function x(){this.y = 1}; x()", + // ThrowStatement + r#"throw new Error(\"Hello Error\")"#, + // TryStatement + r"try {ext()} catch (error) {}", + r"try {} finally {ext()}", + // UnaryExpression + r"!ext()", + r"delete ext.x", + r#"delete ext[\"x\"]"#, + r"const x = ()=>{};delete x()", + // UpdateExpression + r"ext++", + r"const x = {};x[ext()]++", + // VariableDeclaration + r"const x = ext()", + // VariableDeclarator + r"var x = ext(),y = ext()", + r"const x = ext(),y = ext()", + r"let x = ext(),y = ext()", + r"const {x = ext()} = {}", + // WhileStatement + r"while(ext()){}", + r"while(true)ext()", + r"while(true){ext()}", + // YieldExpression + r"function* x(){yield ext()}; x()", + // YieldExpression when called + r"function* x(){yield ext()}; x()", + ]; + + Tester::new(NoSideEffectsInInitialization::NAME, pass, fail).test_and_snapshot(); +} diff --git a/justfile b/justfile index c215046abb1a..b1ae2bef1001 100755 --- a/justfile +++ b/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 diff --git a/tasks/rulegen/src/main.rs b/tasks/rulegen/src/main.rs index 39267a8ff823..f1d6b629b5ee 100644 --- a/tasks/rulegen/src/main.rs +++ b/tasks/rulegen/src/main.rs @@ -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, + group_comment: Option, config: Option>, settings: Option>, } 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() @@ -101,6 +115,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> { @@ -266,23 +284,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, + group_comment_stack: Vec, } 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 { - self.valid_tests.iter().map(|arg| TestCase::new(self.source_text, arg)).collect::>() + self.get_test_cases(&self.valid_tests) } fn fail_cases(&self) -> Vec { - self.invalid_tests + self.get_test_cases(&self.invalid_tests) + } + + fn get_test_cases(&self, tests: &[&'a Expression<'a>]) -> Vec { + 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::>() } + + 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> { @@ -314,21 +365,27 @@ 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 { + 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>) { @@ -339,7 +396,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); } } } @@ -349,7 +406,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); } } } @@ -363,7 +420,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); } } } @@ -375,7 +432,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); } } } @@ -385,7 +442,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); } } } @@ -399,7 +456,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); } } } @@ -453,6 +510,7 @@ pub enum RuleKind { NextJS, JSDoc, Node, + TreeShaking, } impl RuleKind { @@ -469,6 +527,7 @@ impl RuleKind { "nextjs" => Self::NextJS, "jsdoc" => Self::JSDoc, "n" => Self::Node, + "tree-shaking" => Self::TreeShaking, _ => Self::ESLint, } } @@ -489,6 +548,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"), } } } @@ -514,6 +574,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(), }; @@ -547,19 +608,31 @@ 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::>() - .join(",\n"); - - let fail_cases = fail_cases - .into_iter() - .map(|c| c.code(has_config, has_settings)) - .filter(|t| !t.is_empty()) - .collect::>() - .join(",\n"); + let gen_cases_string = |cases: Vec| { + 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 let Some(current_comment) = current_comment { + if !code.is_empty() && current_comment != last_comment { + last_comment = current_comment.to_string(); + code = format!( + "// {}\n{}", + &last_comment, + case.code(has_config, has_settings) + ); + } + } + + 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) } diff --git a/tasks/rulegen/src/template.rs b/tasks/rulegen/src/template.rs index 14e05755a723..646219129802 100644 --- a/tasks/rulegen/src/template.rs +++ b/tasks/rulegen/src/template.rs @@ -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)?;