diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 934febe8a1f60..34bc03c3532ca 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -141,6 +141,7 @@ mod react {
pub mod jsx_key;
pub mod jsx_no_comment_text_nodes;
pub mod jsx_no_duplicate_props;
+ pub mod jsx_no_undef;
pub mod jsx_no_useless_fragment;
pub mod no_children_prop;
pub mod no_dangerously_set_inner_html;
@@ -453,6 +454,7 @@ oxc_macros::declare_all_lint_rules! {
react::jsx_no_comment_text_nodes,
react::jsx_no_duplicate_props,
react::jsx_no_useless_fragment,
+ react::jsx_no_undef,
react::react_in_jsx_scope,
react::no_children_prop,
react::no_dangerously_set_inner_html,
diff --git a/crates/oxc_linter/src/rules/react/jsx_no_undef.rs b/crates/oxc_linter/src/rules/react/jsx_no_undef.rs
new file mode 100644
index 0000000000000..d1ca94db66976
--- /dev/null
+++ b/crates/oxc_linter/src/rules/react/jsx_no_undef.rs
@@ -0,0 +1,125 @@
+use oxc_ast::{
+ ast::{
+ JSXElementName, JSXIdentifier, JSXMemberExpression, JSXMemberExpressionObject,
+ JSXOpeningElement,
+ },
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{Atom, Span};
+
+use crate::{context::LintContext, rule::Rule, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX")]
+#[diagnostic(severity(warning), help("'{0}' is not defined."))]
+struct JsxNoUndefDiagnostic(Atom, #[label] pub Span);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxNoUndef;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// Disallow undeclared variables in JSX
+ ///
+ /// ### Why is this bad?
+ /// It is most likely a potential ReferenceError caused by a misspelling of a variable or parameter name.
+ ///
+ /// ### Example
+ /// ```jsx
+ /// const A = () =>
+ /// const C =
+ /// ```
+ JsxNoUndef,
+ correctness
+);
+
+fn get_member_ident<'a>(expr: &'a JSXMemberExpression<'a>) -> &'a JSXIdentifier {
+ match expr.object {
+ JSXMemberExpressionObject::Identifier(ref ident) => ident,
+ JSXMemberExpressionObject::MemberExpression(ref next_expr) => get_member_ident(next_expr),
+ }
+}
+fn get_resolvable_ident<'a>(node: &'a JSXElementName<'a>) -> Option<&'a JSXIdentifier> {
+ match node {
+ JSXElementName::Identifier(ref ident)
+ if !(ident.name.as_str().starts_with(char::is_lowercase)) =>
+ {
+ Some(ident)
+ }
+ JSXElementName::Identifier(_) | JSXElementName::NamespacedName(_) => None,
+ JSXElementName::MemberExpression(expr) => Some(get_member_ident(expr)),
+ }
+}
+
+impl Rule for JsxNoUndef {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXOpeningElement(JSXOpeningElement { name: el_name, .. }) = &node.kind() {
+ if let Some(ident) = get_resolvable_ident(el_name) {
+ if ident.name.as_str() == "this" {
+ return;
+ }
+ let has_binding = ctx
+ .symbols()
+ .get_scope_id_from_name(&ident.name)
+ .map_or(false, |scope_id| ctx.scopes().has_binding(scope_id, &ident.name));
+
+ if !has_binding {
+ ctx.diagnostic(JsxNoUndefDiagnostic(ident.name.clone(), ident.span));
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ ("var React, App; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React, app; React.render();", None),
+ ("var React, app; React.render();", None),
+ ("var React; React.render();", None),
+ (
+ r"
+ var React;
+ class Hello extends React.Component {
+ render() {
+ return
+ }
+ }
+ ",
+ None,
+ ),
+ // TODO: Text should be declared in globals ("var React; React.render();", None),
+ (
+ r#"
+ import Text from "cool-module";
+ const TextWrapper = function (props) {
+ return (
+
+ );
+ };
+ "#,
+ None,
+ ),
+ ];
+
+ let fail = vec![
+ ("var React; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React; React.render();", None),
+ ("var React; Unknown; React.render()", None),
+ ];
+
+ Tester::new(JsxNoUndef::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_undef.snap b/crates/oxc_linter/src/snapshots/jsx_no_undef.snap
new file mode 100644
index 0000000000000..3b3467c3c53fc
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/jsx_no_undef.snap
@@ -0,0 +1,48 @@
+---
+source: crates/oxc_linter/src/tester.rs
+assertion_line: 144
+expression: jsx_no_undef
+---
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; React.render();
+ · ───
+ ╰────
+ help: 'App' is not defined.
+
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; React.render();
+ · ────
+ ╰────
+ help: 'Appp' is not defined.
+
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; React.render();
+ · ────
+ ╰────
+ help: 'appp' is not defined.
+
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; React.render();
+ · ────
+ ╰────
+ help: 'appp' is not defined.
+
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; React.render();
+ · ───
+ ╰────
+ help: 'Foo' is not defined.
+
+ ⚠ eslint-plugin-react(jsx-no-undef): Disallow undeclared variables in JSX
+ ╭─[jsx_no_undef.tsx:1:1]
+ 1 │ var React; Unknown; React.render()
+ · ───────
+ ╰────
+ help: 'Unknown' is not defined.
+
+