From 75abe5b1c9f4b40248743f2dfaf2a7cdf7fe9cdb Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Fri, 21 Jun 2024 20:47:31 +0200 Subject: [PATCH 1/2] feat(linter/eslint-plugin-react): Implement prefer-es6-class Rule Detail: [link](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/prefer-es6-class.md) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/react/prefer_es6_class.rs | 185 ++++++++++++++++++ .../src/snapshots/prefer_es_6_class.snap | 36 ++++ 3 files changed, 223 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/prefer_es6_class.rs create mode 100644 crates/oxc_linter/src/snapshots/prefer_es_6_class.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index a6d7156367a4c..109e93ddfdc8e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -218,6 +218,7 @@ mod react { pub mod no_string_refs; pub mod no_unescaped_entities; pub mod no_unknown_property; + pub mod prefer_es6_class; pub mod react_in_jsx_scope; pub mod require_render_return; pub mod rules_of_hooks; @@ -686,6 +687,7 @@ oxc_macros::declare_all_lint_rules! { react::no_unescaped_entities, react::no_is_mounted, react::no_unknown_property, + react::prefer_es6_class, react::require_render_return, react::rules_of_hooks, react::void_dom_elements_no_children, diff --git a/crates/oxc_linter/src/rules/react/prefer_es6_class.rs b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs new file mode 100644 index 0000000000000..6510d507ffaae --- /dev/null +++ b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs @@ -0,0 +1,185 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{is_es5_component, is_es6_component}, + AstNode, +}; + +fn unexpected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Component should use createClass instead of es6 class") + .with_label(span0) +} + +fn expected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass") + .with_label(span0) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferEs6Class { + prefer_es6_class_option: PreferES6ClassOptionType, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// React offers you two ways to create traditional components: using the ES5 + /// create-react-class module or the new ES6 class system. + /// + /// ### Why is this bad? + /// + /// This rule enforces a consistent React class style. + /// + /// ### Example + /// ```javascript + /// var Hello = createReactClass({ + /// render: function() { + /// return
Hello {this.props.name}
; + /// } + /// }); + /// ``` + PreferEs6Class, + style, +); + +impl Rule for PreferEs6Class { + fn from_configuration(value: serde_json::Value) -> Self { + let obj = value.get(0); + + Self { + prefer_es6_class_option: obj + .and_then(serde_json::Value::as_str) + .map(PreferES6ClassOptionType::from) + .unwrap_or_default(), + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if matches!(self.prefer_es6_class_option, PreferES6ClassOptionType::Always) { + if is_es5_component(node) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + ctx.diagnostic(expected_es6_class_diagnostic(call_expr.span)); + } + } else if is_es6_component(node) { + let AstKind::Class(class_expr) = node.kind() else { + return; + }; + ctx.diagnostic(unexpected_es6_class_diagnostic(class_expr.span)); + } + } +} + +#[derive(Debug, Default, Clone)] +enum PreferES6ClassOptionType { + #[default] + Always, + Never, +} + +impl PreferES6ClassOptionType { + pub fn from(raw: &str) -> Self { + match raw { + "always" => Self::Always, + _ => Self::Never, + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + r" + class Hello extends React.Component { + render() { + return
Hello {this.props.name}
; + } + } + Hello.displayName = 'Hello' + ", + None, + ), + ( + r" + export default class Hello extends React.Component { + render() { + return
Hello {this.props.name}
; + } + } + Hello.displayName = 'Hello' + ", + None, + ), + ( + r" + var Hello = 'foo'; + module.exports = {}; + ", + None, + ), + ( + r" + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + Some(serde_json::json!(["never"])), + ), + ( + r" + class Hello extends React.Component { + render() { + return
Hello {this.props.name}
; + } + } + ", + Some(serde_json::json!(["always"])), + ), + ]; + + let fail = vec![ + ( + r" + var Hello = createReactClass({ + displayName: 'Hello', + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + r" + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + Some(serde_json::json!(["always"])), + ), + ( + r" + class Hello extends React.Component { + render() { + return
Hello {this.props.name}
; + } + } + ", + Some(serde_json::json!(["never"])), + ), + ]; + + Tester::new(PreferEs6Class::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap new file mode 100644 index 0000000000000..d5eb6d9edc478 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap @@ -0,0 +1,36 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass + ╭─[prefer_es_6_class.tsx:2:25] + 1 │ + 2 │ ╭─▶ var Hello = createReactClass({ + 3 │ │ displayName: 'Hello', + 4 │ │ render: function() { + 5 │ │ return
Hello {this.props.name}
; + 6 │ │ } + 7 │ ╰─▶ }); + 8 │ + ╰──── + + ⚠ eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass + ╭─[prefer_es_6_class.tsx:2:25] + 1 │ + 2 │ ╭─▶ var Hello = createReactClass({ + 3 │ │ render: function() { + 4 │ │ return
Hello {this.props.name}
; + 5 │ │ } + 6 │ ╰─▶ }); + 7 │ + ╰──── + + ⚠ eslint-plugin-react(prefer-es6-class): Component should use createClass instead of es6 class + ╭─[prefer_es_6_class.tsx:2:13] + 1 │ + 2 │ ╭─▶ class Hello extends React.Component { + 3 │ │ render() { + 4 │ │ return
Hello {this.props.name}
; + 5 │ │ } + 6 │ ╰─▶ } + 7 │ + ╰──── From 5f931844c1d38f911596ee86ca508009116d2b9d Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 26 Jun 2024 22:07:16 +0800 Subject: [PATCH 2/2] u --- .../src/rules/react/prefer_es6_class.rs | 13 +++--- .../src/snapshots/prefer_es_6_class.snap | 42 +++++++------------ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/prefer_es6_class.rs b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs index 6510d507ffaae..bcaa346459f40 100644 --- a/crates/oxc_linter/src/rules/react/prefer_es6_class.rs +++ b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs @@ -1,7 +1,7 @@ use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{GetSpan, Span}; use crate::{ context::LintContext, @@ -11,12 +11,12 @@ use crate::{ }; fn unexpected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Component should use createClass instead of es6 class") + OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Components should use createClass instead of ES6 class.") .with_label(span0) } fn expected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass") + OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass.") .with_label(span0) } @@ -58,19 +58,22 @@ impl Rule for PreferEs6Class { .unwrap_or_default(), } } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { if matches!(self.prefer_es6_class_option, PreferES6ClassOptionType::Always) { if is_es5_component(node) { let AstKind::CallExpression(call_expr) = node.kind() else { return; }; - ctx.diagnostic(expected_es6_class_diagnostic(call_expr.span)); + ctx.diagnostic(expected_es6_class_diagnostic(call_expr.callee.span())); } } else if is_es6_component(node) { let AstKind::Class(class_expr) = node.kind() else { return; }; - ctx.diagnostic(unexpected_es6_class_diagnostic(class_expr.span)); + ctx.diagnostic(unexpected_es6_class_diagnostic( + class_expr.id.as_ref().map_or(class_expr.span, |id| id.span), + )); } } } diff --git a/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap index d5eb6d9edc478..167561671b1ca 100644 --- a/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap +++ b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap @@ -1,36 +1,26 @@ --- source: crates/oxc_linter/src/tester.rs --- - ⚠ eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass + ⚠ eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass. ╭─[prefer_es_6_class.tsx:2:25] - 1 │ - 2 │ ╭─▶ var Hello = createReactClass({ - 3 │ │ displayName: 'Hello', - 4 │ │ render: function() { - 5 │ │ return
Hello {this.props.name}
; - 6 │ │ } - 7 │ ╰─▶ }); - 8 │ + 1 │ + 2 │ var Hello = createReactClass({ + · ──────────────── + 3 │ displayName: 'Hello', ╰──── - ⚠ eslint-plugin-react(prefer-es6-class): Component should use es6 class instead of createClass + ⚠ eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass. ╭─[prefer_es_6_class.tsx:2:25] - 1 │ - 2 │ ╭─▶ var Hello = createReactClass({ - 3 │ │ render: function() { - 4 │ │ return
Hello {this.props.name}
; - 5 │ │ } - 6 │ ╰─▶ }); - 7 │ + 1 │ + 2 │ var Hello = createReactClass({ + · ──────────────── + 3 │ render: function() { ╰──── - ⚠ eslint-plugin-react(prefer-es6-class): Component should use createClass instead of es6 class - ╭─[prefer_es_6_class.tsx:2:13] - 1 │ - 2 │ ╭─▶ class Hello extends React.Component { - 3 │ │ render() { - 4 │ │ return
Hello {this.props.name}
; - 5 │ │ } - 6 │ ╰─▶ } - 7 │ + ⚠ eslint-plugin-react(prefer-es6-class): Components should use createClass instead of ES6 class. + ╭─[prefer_es_6_class.tsx:2:19] + 1 │ + 2 │ class Hello extends React.Component { + · ───── + 3 │ render() { ╰────