feat(task): init eslint-plugin-tree-shaking rule (#2662)

This commit is contained in:
Wang Wenzhe 2024-03-10 22:07:34 +08:00 committed by GitHub
parent 588e94604c
commit f8e8af2a66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 732 additions and 38 deletions

View file

@ -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,
}

View file

@ -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();
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)?;