diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 69470e820..548e71c54 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -132,6 +132,7 @@ mod react {
pub mod no_children_prop;
pub mod no_dangerously_set_inner_html;
pub mod no_find_dom_node;
+ pub mod no_unescaped_entities;
}
mod unicorn {
@@ -258,6 +259,7 @@ oxc_macros::declare_all_lint_rules! {
react::jsx_key,
react::jsx_no_comment_text_nodes,
react::jsx_no_duplicate_props,
+ react::no_unescaped_entities,
react::jsx_no_useless_fragment,
react::no_children_prop,
react::no_dangerously_set_inner_html,
diff --git a/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs b/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs
new file mode 100644
index 000000000..bac66deca
--- /dev/null
+++ b/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs
@@ -0,0 +1,187 @@
+use oxc_ast::AstKind;
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::{self, Error},
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+use phf::{phf_map, Map};
+
+use crate::{context::LintContext, rule::Rule, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-react(no-unescaped-entities): `{1}` can be escaped with {2}")]
+#[diagnostic(severity(warning))]
+struct NoUnescapedEntitiesDiagnostic(#[label] pub Span, pub char, pub String);
+
+#[derive(Debug, Default, Clone)]
+pub struct NoUnescapedEntities;
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// This rule prevents characters that you may have meant as JSX escape characters from being accidentally injected as a text node in JSX statements.
+ ///
+ /// ### Why is this bad?
+ ///
+ /// JSX escape characters are used to inject characters into JSX statements that would otherwise be interpreted as code.
+ ///
+ /// ### Example
+ /// Incorrect
+ ///
+ /// ```jsx
+ ///
>
+ /// ```
+ ///
+ /// Correct
+ ///
+ /// ```jsx
+ /// >
+ /// ```
+ ///
+ /// ```jsx
+ /// {'>'}
+ /// ```
+ NoUnescapedEntities,
+ correctness
+);
+
+impl Rule for NoUnescapedEntities {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXText(jsx_text) = node.kind() {
+ let source = jsx_text.span.source_text(ctx.source_text());
+
+ for (i, char) in source.chars().enumerate() {
+ if let Some(escapes) = DEFAULTS.get(&char) {
+ #[allow(clippy::cast_possible_truncation)]
+ ctx.diagnostic(NoUnescapedEntitiesDiagnostic(
+ Span {
+ start: jsx_text.span.start + i as u32,
+ end: jsx_text.span.start + i as u32 + 1,
+ },
+ char,
+ escapes.join(" or "),
+ ));
+ }
+ }
+ }
+ }
+}
+
+pub const DEFAULTS: Map = phf_map! {
+ '>' => &[">"],
+ '"' => &[""", "“", """, "”"],
+ '\'' => &["'", "‘", "'", "’"],
+ '}' => &["}"],
+};
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return (
+
+ );
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return Here is some text!
;
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return I’ve escaped some entities: > < &
;
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return first line is ok
+ so is second
+ and here are some escaped entities: > < &
;
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return {\">\" + \"<\" + \"&\" + '\"'}
;
+ },
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return <>Here is some text!>;
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return <>I’ve escaped some entities: > < &>;
+ }
+ });
+ ",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return <>{\">\" + \"<\" + \"&\" + '\"'}>;
+ },
+ });
+ ",
+ ];
+
+ let fail = vec![
+ "var Hello = createReactClass({
+ render: function() {
+ return <>> babel-eslint>;
+ }
+ });",
+ "var Hello = createReactClass({
+ render: function() {
+ return <>first line is ok
+ so is second
+ and here are some bad entities: >>
+ }
+ });",
+ "
+ var Hello = createReactClass({
+ render: function() {
+ return '
;
+ }
+ });
+ ",
+ r#"
+ var Hello = createReactClass({
+ render: function() {
+ return <>{"Unbalanced braces - babel-eslint"}}>;
+ }
+ });
+ "#,
+ // "var Hello = createReactClass({
+ // render: function() {
+ // return <>foo & bar>;
+ // }
+ // });",
+ // " var Hello = createReactClass({
+ // render: function() {
+ // return foo & bar;
+ // }
+ // });
+ // ",
+ r#""#,
+ ];
+
+ Tester::new_without_config(NoUnescapedEntities::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap b/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap
new file mode 100644
index 000000000..93e33b37a
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap
@@ -0,0 +1,49 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: no_unescaped_entities
+---
+ ⚠ eslint-plugin-react(no-unescaped-entities): `>` can be escaped with >
+ ╭─[no_unescaped_entities.tsx:2:1]
+ 2 │ render: function() {
+ 3 │ return <>> babel-eslint>;
+ · ─
+ 4 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(no-unescaped-entities): `>` can be escaped with >
+ ╭─[no_unescaped_entities.tsx:4:1]
+ 4 │ so is second
+ 5 │ and here are some bad entities: >>
+ · ─
+ 6 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(no-unescaped-entities): `'` can be escaped with ' or ‘ or ' or ’
+ ╭─[no_unescaped_entities.tsx:3:1]
+ 3 │ render: function() {
+ 4 │ return '
;
+ · ─
+ 5 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(no-unescaped-entities): `}` can be escaped with }
+ ╭─[no_unescaped_entities.tsx:3:1]
+ 3 │ render: function() {
+ 4 │ return <>{"Unbalanced braces - babel-eslint"}}>;
+ · ─
+ 5 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(no-unescaped-entities): `"` can be escaped with " or “ or " or ”
+ ╭─[no_unescaped_entities.tsx:1:1]
+ 1 │
+ · ─
+ ╰────
+
+ ⚠ eslint-plugin-react(no-unescaped-entities): `"` can be escaped with " or “ or " or ”
+ ╭─[no_unescaped_entities.tsx:1:1]
+ 1 │
+ · ─
+ ╰────
+
+