Skip to content

Commit

Permalink
feat(linter) eslint-plugin-react: react-in-jsx-scope (#1025)
Browse files Browse the repository at this point in the history
Co-authored-by: wenzhe <mysteryven@gmail.com>
  • Loading branch information
camc314 and mysteryven committed Dec 10, 2023
1 parent b425b73 commit 9bea278
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ mod react {
pub mod no_render_return_value;
pub mod no_string_refs;
pub mod no_unescaped_entities;
pub mod react_in_jsx_scope;
}

mod unicorn {
Expand Down Expand Up @@ -418,6 +419,7 @@ oxc_macros::declare_all_lint_rules! {
react::jsx_no_comment_text_nodes,
react::jsx_no_duplicate_props,
react::jsx_no_useless_fragment,
react::react_in_jsx_scope,
react::no_children_prop,
react::no_dangerously_set_inner_html,
react::no_find_dom_node,
Expand Down
102 changes: 102 additions & 0 deletions crates/oxc_linter/src/rules/react/react_in_jsx_scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use oxc_ast::AstKind;
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::Error,
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, GetSpan, Span};

use crate::{context::LintContext, rule::Rule, AstNode};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX")]
#[diagnostic(severity(warning), help("When using JSX, `<a />` expands to `React.createElement(\"a\")`. Therefore the `React` variable must be in scope."))]
struct ReactInJsxScopeDiagnostic(#[label] pub Span);

#[derive(Debug, Default, Clone)]
pub struct ReactInJsxScope;

declare_oxc_lint!(
/// ### What it does
///
/// Disallow missing React when using JSX
///
/// ### Why is this bad?
///
/// When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.
///
/// ### Example
/// ```javascript
/// // Bad
/// var a = <a />;
///
/// // Good
/// import React from "react";
/// var a = <a />;
///
/// ```
ReactInJsxScope,
suspicious
);

impl Rule for ReactInJsxScope {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let node_span = match node.kind() {
AstKind::JSXOpeningElement(v) => v.name.span(),
AstKind::JSXFragment(v) => v.opening_fragment.span,
_ => return,
};
let scope = ctx.scopes();
let react_name: &Atom = &Atom::from("React");
if scope.get_binding(scope.root_scope_id(), react_name).is_some() {
return;
}

if !scope
.ancestors(node.scope_id())
.any(|v| scope.get_bindings(v).iter().any(|(k, _)| k == react_name))
{
ctx.diagnostic(ReactInJsxScopeDiagnostic(node_span));
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
("var React, App; <App />;", None),
("var React; <img />;", None),
("var React; <>fragment</>;", None),
("var React; <x-gif />;", None),
("var React, App, a=1; <App attr={a} />;", None),
("var React, App, a=1; function elem() { return <App attr={a} />; }", None),
("var React, App; <App />;", None),
(
"
import React from 'react/addons';
const Button = createReactClass({
render() {
return (
<button {...this.props}>{this.props.children}</button>
)
}
});
export default Button;
",
None,
),
("var React, a = <img />;", None),
];

let fail = vec![
("var App, a = <App />;", None),
("var a = <App />;", None),
("var a = <img />;", None),
("var a = <>fragment</>;", None),
("var Foo, a = <img />;", None),
];

Tester::new(ReactInJsxScope::NAME, pass, fail).test_and_snapshot();
}
40 changes: 40 additions & 0 deletions crates/oxc_linter/src/snapshots/react_in_jsx_scope.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
source: crates/oxc_linter/src/tester.rs
expression: react_in_jsx_scope
---
eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX
╭─[react_in_jsx_scope.tsx:1:1]
1var App, a = <App />;
· ───
╰────
help: When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.

eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX
╭─[react_in_jsx_scope.tsx:1:1]
1var a = <App />;
· ───
╰────
help: When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.

eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX
╭─[react_in_jsx_scope.tsx:1:1]
1var a = <img />;
· ───
╰────
help: When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.

eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX
╭─[react_in_jsx_scope.tsx:1:1]
1var a = <>fragment</>;
· ──
╰────
help: When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.

eslint-plugin-react(react-in-jsx-scope): 'React' must be in scope when using JSX
╭─[react_in_jsx_scope.tsx:1:1]
1var Foo, a = <img />;
· ───
╰────
help: When using JSX, `<a />` expands to `React.createElement("a")`. Therefore the `React` variable must be in scope.


0 comments on commit 9bea278

Please sign in to comment.