diff --git a/crates/oxc_ast/src/ast_kind.rs b/crates/oxc_ast/src/ast_kind.rs
index f668e083b..98bd3b26b 100644
--- a/crates/oxc_ast/src/ast_kind.rs
+++ b/crates/oxc_ast/src/ast_kind.rs
@@ -114,6 +114,7 @@ pub enum AstKind<'a> {
JSXOpeningElement(&'a JSXOpeningElement<'a>),
JSXElementName(&'a JSXElementName<'a>),
JSXAttributeItem(&'a JSXAttributeItem<'a>),
+ JSXText(&'a JSXText),
// TypeScript
TSModuleBlock(&'a TSModuleBlock<'a>),
@@ -240,6 +241,7 @@ impl<'a> AstKind<'a> {
| Self::JSXElementName(_)
| Self::JSXFragment(_)
| Self::JSXAttributeItem(_)
+ | Self::JSXText(_)
)
}
@@ -361,6 +363,7 @@ impl<'a> GetSpan for AstKind<'a> {
Self::JSXElement(x) => x.span,
Self::JSXFragment(x) => x.span,
Self::JSXAttributeItem(x) => x.span(),
+ Self::JSXText(x) => x.span,
Self::TSModuleBlock(x) => x.span,
@@ -526,6 +529,7 @@ impl<'a> AstKind<'a> {
Self::JSXElement(_) => "JSXElement".into(),
Self::JSXFragment(_) => "JSXFragment".into(),
Self::JSXAttributeItem(_) => "JSXAttributeItem".into(),
+ Self::JSXText(_) => "JSXText".into(),
Self::TSModuleBlock(_) => "TSModuleBlock".into(),
diff --git a/crates/oxc_ast/src/visit.rs b/crates/oxc_ast/src/visit.rs
index b3f75e463..af82e9e68 100644
--- a/crates/oxc_ast/src/visit.rs
+++ b/crates/oxc_ast/src/visit.rs
@@ -1094,7 +1094,7 @@ pub trait Visit<'a>: Sized {
JSXChild::Fragment(elem) => self.visit_jsx_fragment(elem),
JSXChild::ExpressionContainer(expr) => self.visit_jsx_expression_container(expr),
JSXChild::Spread(expr) => self.visit_jsx_spread_child(expr),
- JSXChild::Text(_) => {}
+ JSXChild::Text(expr) => self.visit_jsx_text(expr),
}
}
@@ -1102,6 +1102,12 @@ pub trait Visit<'a>: Sized {
self.visit_expression(&child.expression);
}
+ fn visit_jsx_text(&mut self, child: &JSXText) {
+ let kind = AstKind::JSXText(self.alloc(child));
+ self.enter_node(kind);
+ self.leave_node(kind);
+ }
+
/* ---------- Pattern ---------- */
fn visit_binding_pattern(&mut self, pat: &BindingPattern<'a>) {
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index d7123f0f3..b795e250a 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -126,6 +126,7 @@ mod jest {
mod react {
pub mod jsx_key;
+ pub mod jsx_no_comment_text_nodes;
pub mod jsx_no_duplicate_props;
pub mod jsx_no_useless_fragment;
pub mod no_children_prop;
@@ -254,6 +255,7 @@ oxc_macros::declare_all_lint_rules! {
unicorn::throw_new_error,
unicorn::prefer_array_flat_map,
react::jsx_key,
+ react::jsx_no_comment_text_nodes,
react::jsx_no_duplicate_props,
react::jsx_no_useless_fragment,
react::no_children_prop,
diff --git a/crates/oxc_linter/src/rules/react/jsx_no_comment_text_nodes.rs b/crates/oxc_linter/src/rules/react/jsx_no_comment_text_nodes.rs
new file mode 100644
index 000000000..711827201
--- /dev/null
+++ b/crates/oxc_linter/src/rules/react/jsx_no_comment_text_nodes.rs
@@ -0,0 +1,313 @@
+use lazy_static::lazy_static;
+use oxc_ast::AstKind;
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{Atom, Span};
+use regex::Regex;
+
+use crate::{context::LintContext, rule::Rule, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces")]
+#[diagnostic(severity(warning))]
+struct JsxNoCommentTextNodesDiagnostic(#[label] pub Span);
+
+#[derive(Debug, Default, Clone)]
+pub struct JsxNoCommentTextNodes;
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// This rule prevents comment strings (e.g. beginning with `//` or `/*`) from being accidentally injected as a text node in JSX statements.
+ ///
+ /// ### Why is this bad?
+ ///
+ /// In JSX, any text node that is not wrapped in curly braces is considered a literal string to be rendered. This can lead to unexpected behavior when the text contains a comment.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Incorrect:
+ ///
+ /// const Hello = () => {
+ /// return
// empty div
;
+ /// }
+ ///
+ /// const Hello = () => {
+ /// return /* empty div */
;
+ /// }
+ ///
+ /// // Correct:
+ ///
+ /// const Hello = () => {
+ /// return // empty div
;
+ /// }
+ ///
+ /// const Hello = () => {
+ /// return {/* empty div */}
;
+ /// }
+ /// ```
+ JsxNoCommentTextNodes,
+ suspicious
+);
+
+impl Rule for JsxNoCommentTextNodes {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ let AstKind::JSXText(jsx_text) = node.kind() else { return };
+
+ if control_patterns(&jsx_text.value) {
+ ctx.diagnostic(JsxNoCommentTextNodesDiagnostic(jsx_text.span));
+ }
+ }
+}
+
+fn control_patterns(pattern: &Atom) -> bool {
+ lazy_static! {
+ static ref CTL_PAT: Regex = Regex::new(r"(?m)^\s*/(/|\*)",).unwrap();
+ }
+ CTL_PAT.is_match(pattern.as_str())
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+ {/* valid */}
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+ <>
+ {/* valid */}
+ >
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return ({/* valid */}
);
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ const bar = ({/* valid */}
);
+ return bar;
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ var Hello = createReactClass({
+ foo: ({/* valid */}
),
+ render() {
+ return this.foo;
+ },
+ });
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+ {/* valid */}
+ {/* valid 2 */}
+ {/* valid 3 */}
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ var foo = require('foo');
+ ",
+ None,
+ ),
+ (
+ "
+
+ {/* valid */}
+
+ ",
+ None,
+ ),
+ (
+ "
+
+ https://www.example.com/attachment/download/1
+
+ ",
+ None,
+ ),
+ (
+ "
+
+ ",
+ None,
+ ),
+ (
+ "
+ * valid */>>
+ ",
+ None,
+ ),
+ (
+ "
+ <>* valid *//>
+ ",
+ None,
+ ),
+ (
+ "
+
+ ",
+ None,
+ ),
+ ("// TODO: Write perfect code
", None),
+ ("/* TODO: Write perfect code */
", None),
+ (
+ "
+
+ // ...
+
+ ",
+ None,
+ ),
+ ];
+
+ let fail = vec![
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (// invalid
);
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (<>// invalid>);
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (/* invalid */
);
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+ // invalid
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+ asdjfl
+ /* invalid */
+ foo
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ class Comp1 extends Component {
+ render() {
+ return (
+
+ {'asdjfl'}
+ // invalid
+ {'foo'}
+
+ );
+ }
+ }
+ ",
+ None,
+ ),
+ (
+ "
+ const Component2 = () => {
+ return /*;
+ };
+ ",
+ None,
+ ),
+ ];
+
+ Tester::new(JsxNoCommentTextNodes::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_comment_text_nodes.snap b/crates/oxc_linter/src/snapshots/jsx_no_comment_text_nodes.snap
new file mode 100644
index 000000000..b0a0db8d1
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/jsx_no_comment_text_nodes.snap
@@ -0,0 +1,66 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: jsx_no_comment_text_nodes
+---
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:3:1]
+ 3 │ render() {
+ 4 │ return (// invalid
);
+ · ──────────
+ 5 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:3:1]
+ 3 │ render() {
+ 4 │ return (<>// invalid>);
+ · ──────────
+ 5 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:3:1]
+ 3 │ render() {
+ 4 │ return (/* invalid */
);
+ · ─────────────
+ 5 │ }
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:4:1]
+ 4 │ return (
+ 5 │ ╭─▶
+ 6 │ │ // invalid
+ 7 │ ╰─▶
+ 8 │ );
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:4:1]
+ 4 │ return (
+ 5 │ ╭─▶
+ 6 │ │ asdjfl
+ 7 │ │ /* invalid */
+ 8 │ │ foo
+ 9 │ ╰─▶
+ 10 │ );
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:5:1]
+ 5 │
+ 6 │ ╭─▶ {'asdjfl'}
+ 7 │ │ // invalid
+ 8 │ ╰─▶ {'foo'}
+ 9 │
+ ╰────
+
+ ⚠ eslint-plugin-react(jsx-no-comment-TextNodes): Comments inside children section of tag should be placed inside braces
+ ╭─[jsx_no_comment_text_nodes.tsx:2:1]
+ 2 │ const Component2 = () => {
+ 3 │ return /*;
+ · ──
+ 4 │ };
+ ╰────
+
+