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..bcaa346459f40 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs @@ -0,0 +1,188 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, 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): 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): Components 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.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.id.as_ref().map_or(class_expr.span, |id| id.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..167561671b1ca --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap @@ -0,0 +1,26 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ 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', + ╰──── + + ⚠ 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() { + ╰──── + + ⚠ 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() { + ╰────