feat(linter): eslint-plugin-react no-string-refs (#1053)

Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
Trevor Manz 2023-10-24 23:53:46 -04:00 committed by GitHub
parent 7192520d2b
commit ebab50e0ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 353 additions and 0 deletions

View file

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

View file

@ -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 <div ref="hello">Hello, world.</div>;
/// },
/// });
///
/// // Good
/// var Hello = createReactClass({
/// componentDidMount: function() {
/// var component = this.hello;
/// // ...do something with component
/// },
/// render: function() {
/// return <div ref={c => this.hello = c}>Hello, world.</div>;
/// },
/// });
/// ```
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 <div>Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
componentDidMount: function() {
var component = this.hello;
},
render: function() {
return <div ref={c => this.hello = c}>Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
render: function() {
return <div ref={`hello`}>Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
render: function() {
return <div ref={`hello${index}`}>Hello {this.props.name}</div>;
}
});
",
None,
),
];
let fail = vec![
(
"
var Hello = createReactClass({
componentDidMount: function() {
var component = this.refs.hello;
},
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
render: function() {
return <div ref=\"hello\">Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
render: function() {
return <div ref={'hello'}>Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
componentDidMount: function() {
var component = this.refs.hello;
},
render: function() {
return <div ref=\"hello\">Hello {this.props.name}</div>;
}
});
",
None,
),
(
"
var Hello = createReactClass({
componentDidMount: function() {
var component = this.refs.hello;
},
render: function() {
return <div ref={`hello`}>Hello {this.props.name}</div>;
}
});
",
Some(serde_json::json!([{ "noTemplateLiterals": true }])),
),
(
"
var Hello = createReactClass({
componentDidMount: function() {
var component = this.refs.hello;
},
render: function() {
return <div ref={`hello${index}`}>Hello {this.props.name}</div>;
}
});
",
Some(serde_json::json!([{ "noTemplateLiterals": true }])),
),
];
Tester::new(NoStringRefs::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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 <div ref="hello">Hello {this.props.name}</div>;
· ───────
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 <div ref={'hello'}>Hello {this.props.name}</div>;
· ───────
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 <div ref="hello">Hello {this.props.name}</div>;
· ───────
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 <div ref={`hello`}>Hello {this.props.name}</div>;
· ───────
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 <div ref={`hello${index}`}>Hello {this.props.name}</div>;
· ───────────────
8 │ }
╰────
help: Using string literals in ref attributes is deprecated. Use a callback instead.