diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index f5b784f6b..e8fd84a2a 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 000000000..26e950ae4 --- /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 + "[]", + "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 = "#, + "class X {}; const x = ", + "class X {}; const x = />", + // JSXElement + "class X {}; const x = ", + "class X {}; const x = Text", + // JSXEmptyExpression + "class X {}; const x = {}", + // JSXExpressionContainer + "class X {}; const x = {3}", + // JSXIdentifier + "class X {}; const x = ", + "const X = class {constructor() {this.x = 1}}; const x = ", + // JSXOpeningElement + "class X {}; const x = ", + "class X {}; const x = ", + r#"class X {}; const x = "#, + // JSXSpreadAttribute + "class X {}; const x = ", + // 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 = ", + "class X {}; class Y {constructor(){ext()}}; const x = />", + // JSXElement + "class X {constructor(){ext()}}; const x = ", + "class X {}; const x = {ext()}", + // JSXExpressionContainer + "class X {}; const x = {ext()}", + // JSXIdentifier + "class X {constructor(){ext()}}; const x = ", + "const X = class {constructor(){ext()}}; const x = ", + "const x = ", + // JSXMemberExpression + "const X = {Y: ext}; const x = ", + // JSXOpeningElement + "class X {}; const x = ", + // JSXSpreadAttribute + "class X {}; const x = ", + // 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(); +} diff --git a/justfile b/justfile index c215046ab..b1ae2bef1 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 a0b4a14ce..23dca8638 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() @@ -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, + 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> { @@ -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::>() - .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 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::>() - .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) } diff --git a/tasks/rulegen/src/template.rs b/tasks/rulegen/src/template.rs index 14e05755a..646219129 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)?;