diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 0d464e11e..fa7273d05 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -133,6 +133,7 @@ mod react { pub mod no_dangerously_set_inner_html; pub mod no_find_dom_node; pub mod no_render_return_value; + pub mod no_string_refs; pub mod no_unescaped_entities; } @@ -266,6 +267,7 @@ oxc_macros::declare_all_lint_rules! { react::no_dangerously_set_inner_html, react::no_find_dom_node, react::no_render_return_value, + react::no_string_refs, import::named, import::no_cycle, import::no_self_import, diff --git a/crates/oxc_linter/src/rules/react/no_string_refs.rs b/crates/oxc_linter/src/rules/react/no_string_refs.rs new file mode 100644 index 000000000..3c1560e38 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_string_refs.rs @@ -0,0 +1,265 @@ +use oxc_ast::ast::{CallExpression, JSXAttributeItem, JSXAttributeName}; +use oxc_ast::{ + ast::{Expression, JSXAttributeValue, JSXExpression, JSXExpressionContainer, MemberExpression}, + AstKind, +}; +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-react(no-string-refs):")] +#[diagnostic( + severity(warning), + help("Using string literals in ref attributes is deprecated. Use a callback instead.") +)] +struct NoStringRefsDiagnostic(#[label] pub Span); + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-react(no-string-refs):")] +#[diagnostic(severity(warning), help("Using this.refs is deprecated."))] +struct NoThisRefsDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoStringRefs { + /// When set to `true`, it will give a warning when using template literals for refs. + pub no_template_literals: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule disallows using string references in JSX `ref` attributes. + /// + /// ### Why is this bad? + /// + /// String refs are considered legacy in the React documentation. Callback refs are preferred. + /// + /// ### Example + /// ```javascript + /// // Bad + /// var Hello = createReactClass({ + /// componentDidMount: function() { + /// var component = this.refs.hello; + /// // ...do something with component + /// }, + /// render: function() { + /// return
Hello, world.
; + /// }, + /// }); + /// + /// // Good + /// var Hello = createReactClass({ + /// componentDidMount: function() { + /// var component = this.hello; + /// // ...do something with component + /// }, + /// render: function() { + /// return
this.hello = c}>Hello, world.
; + /// }, + /// }); + /// ``` + NoStringRefs, + correctness +); + +impl Rule for NoStringRefs { + fn from_configuration(value: serde_json::Value) -> Self { + Self { + no_template_literals: value.get(0).and_then(|x| x.get("noTemplateLiterals")).map_or( + false, + |x| match x { + serde_json::Value::Bool(b) => *b, + _ => false, + }, + ), + } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(attribute)) = node.kind() { + let Some(value) = &attribute.0.value else { + return; + }; + if let JSXAttributeName::Identifier(iden) = &attribute.0.name { + if iden.name.as_str() != "ref" { + return; + }; + } + match value { + JSXAttributeValue::StringLiteral(s) => { + ctx.diagnostic(NoStringRefsDiagnostic(s.span)); + } + JSXAttributeValue::ExpressionContainer(JSXExpressionContainer { + expression: JSXExpression::Expression(expr), + .. + }) => match expr { + Expression::TemplateLiteral(s) if self.no_template_literals => { + ctx.diagnostic(NoStringRefsDiagnostic(s.span)); + } + Expression::StringLiteral(s) => { + ctx.diagnostic(NoStringRefsDiagnostic(s.span)); + } + _ => return, + }, + _ => return, + } + } + + if let AstKind::MemberExpression(MemberExpression::StaticMemberExpression(expr)) = + node.kind() + { + if let (&Expression::ThisExpression(_), "refs") = + (&expr.object, expr.property.name.as_str()) + { + for node_id in ctx.nodes().ancestors(node.id()).skip(1) { + let parent = ctx.nodes().get_node(node_id); + if let AstKind::CallExpression(CallExpression { + callee: Expression::Identifier(iden), + .. + }) = parent.kind() + { + if iden.name.as_str() == "createReactClass" { + ctx.diagnostic(NoThisRefsDiagnostic(expr.span)); + } + } + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + var Hello = blah({ + componentDidMount: function() { + var component = this.refs.hello; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + componentDidMount: function() { + var component = this.hello; + }, + render: function() { + return
this.hello = c}>Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ]; + + let fail = vec![ + ( + " + var Hello = createReactClass({ + componentDidMount: function() { + var component = this.refs.hello; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + componentDidMount: function() { + var component = this.refs.hello; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + None, + ), + ( + " + var Hello = createReactClass({ + componentDidMount: function() { + var component = this.refs.hello; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + Some(serde_json::json!([{ "noTemplateLiterals": true }])), + ), + ( + " + var Hello = createReactClass({ + componentDidMount: function() { + var component = this.refs.hello; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + Some(serde_json::json!([{ "noTemplateLiterals": true }])), + ), + ]; + + Tester::new(NoStringRefs::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_string_refs.snap b/crates/oxc_linter/src/snapshots/no_string_refs.snap new file mode 100644 index 000000000..08cdbe974 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_string_refs.snap @@ -0,0 +1,86 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_string_refs +--- + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ var component = this.refs.hello; + · ───────── + 5 │ }, + ╰──── + help: Using this.refs is deprecated. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ render: function() { + 4 │ return
Hello {this.props.name}
; + · ─────── + 5 │ } + ╰──── + help: Using string literals in ref attributes is deprecated. Use a callback instead. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ render: function() { + 4 │ return
Hello {this.props.name}
; + · ─────── + 5 │ } + ╰──── + help: Using string literals in ref attributes is deprecated. Use a callback instead. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ var component = this.refs.hello; + · ───────── + 5 │ }, + ╰──── + help: Using this.refs is deprecated. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:6:1] + 6 │ render: function() { + 7 │ return
Hello {this.props.name}
; + · ─────── + 8 │ } + ╰──── + help: Using string literals in ref attributes is deprecated. Use a callback instead. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ var component = this.refs.hello; + · ───────── + 5 │ }, + ╰──── + help: Using this.refs is deprecated. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:6:1] + 6 │ render: function() { + 7 │ return
Hello {this.props.name}
; + · ─────── + 8 │ } + ╰──── + help: Using string literals in ref attributes is deprecated. Use a callback instead. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ var component = this.refs.hello; + · ───────── + 5 │ }, + ╰──── + help: Using this.refs is deprecated. + + ⚠ eslint-plugin-react(no-string-refs): + ╭─[no_string_refs.tsx:6:1] + 6 │ render: function() { + 7 │ return
Hello {this.props.name}
; + · ─────────────── + 8 │ } + ╰──── + help: Using string literals in ref attributes is deprecated. Use a callback instead. + +