diff --git a/docs/rules/jsx_curly_braces.md b/docs/rules/jsx_curly_braces.md
new file mode 100644
index 00000000..cffb17e1
--- /dev/null
+++ b/docs/rules/jsx_curly_braces.md
@@ -0,0 +1,17 @@
+Ensure consistent use of curly braces around JSX expressions.
+
+### Invalid:
+
+```tsx
+const foo = />;
+const foo = ;
+const foo =
{"foo"}
;
+```
+
+### Valid:
+
+```tsx
+const foo = } />;
+const foo = ;
+const foo = foo
;
+```
diff --git a/src/rules.rs b/src/rules.rs
index 2b6a4a07..8ac0b981 100644
--- a/src/rules.rs
+++ b/src/rules.rs
@@ -24,6 +24,7 @@ pub mod fresh_handler_export;
pub mod fresh_server_event_handlers;
pub mod getter_return;
pub mod guard_for_in;
+pub mod jsx_curly_braces;
pub mod no_array_constructor;
pub mod no_async_promise_executor;
pub mod no_await_in_loop;
@@ -255,6 +256,7 @@ fn get_all_rules_raw() -> Vec> {
Box::new(fresh_server_event_handlers::FreshServerEventHandlers),
Box::new(getter_return::GetterReturn),
Box::new(guard_for_in::GuardForIn),
+ Box::new(jsx_curly_braces::JSXCurlyBraces),
Box::new(no_array_constructor::NoArrayConstructor),
Box::new(no_async_promise_executor::NoAsyncPromiseExecutor),
Box::new(no_await_in_loop::NoAwaitInLoop),
diff --git a/src/rules/jsx_curly_braces.rs b/src/rules/jsx_curly_braces.rs
new file mode 100644
index 00000000..46b64991
--- /dev/null
+++ b/src/rules/jsx_curly_braces.rs
@@ -0,0 +1,203 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use super::{Context, LintRule};
+use crate::diagnostic::{LintFix, LintFixChange};
+use crate::handler::{Handler, Traverse};
+use crate::Program;
+use deno_ast::view::{
+ Expr, JSXAttr, JSXAttrValue, JSXElement, JSXElementChild, JSXExpr, Lit,
+ NodeTrait,
+};
+use deno_ast::SourceRanged;
+
+#[derive(Debug)]
+pub struct JSXCurlyBraces;
+
+const CODE: &str = "jsx-curly-braces";
+
+impl LintRule for JSXCurlyBraces {
+ fn tags(&self) -> &'static [&'static str] {
+ &["react", "jsx"]
+ }
+
+ fn code(&self) -> &'static str {
+ CODE
+ }
+
+ fn lint_program_with_ast_view(
+ &self,
+ context: &mut Context,
+ program: Program,
+ ) {
+ JSXCurlyBracesHandler.traverse(program, context);
+ }
+
+ #[cfg(feature = "docs")]
+ fn docs(&self) -> &'static str {
+ include_str!("../../docs/rules/jsx_curly_braces.md")
+ }
+}
+
+enum DiagnosticKind {
+ CurlyAttribute,
+ CurlyChild,
+ MissingCurlyAttribute,
+}
+
+impl DiagnosticKind {
+ fn message(&self) -> &'static str {
+ match *self {
+ DiagnosticKind::CurlyAttribute => "Curly braces are not needed here",
+ DiagnosticKind::MissingCurlyAttribute => {
+ "Missing curly braces around JSX attribute value"
+ }
+ DiagnosticKind::CurlyChild => {
+ "Found curly braces around JSX child literal"
+ }
+ }
+ }
+
+ fn hint(&self) -> &'static str {
+ match *self {
+ DiagnosticKind::CurlyAttribute => {
+ "Remove curly braces around JSX attribute"
+ }
+ DiagnosticKind::MissingCurlyAttribute => {
+ "Remove curly braces around JSX child"
+ }
+ DiagnosticKind::CurlyChild => "Remove curly braces around JSX child",
+ }
+ }
+}
+
+struct JSXCurlyBracesHandler;
+
+impl Handler for JSXCurlyBracesHandler {
+ fn jsx_element(&mut self, node: &JSXElement, ctx: &mut Context) {
+ for child in node.children {
+ if let JSXElementChild::JSXExprContainer(child_expr) = child {
+ if let JSXExpr::Expr(expr) = child_expr.expr {
+ if let Expr::Lit(lit) = expr {
+ if let Lit::Str(lit_str) = lit {
+ ctx.add_diagnostic_with_fixes(
+ child.range(),
+ CODE,
+ DiagnosticKind::CurlyChild.message(),
+ Some(DiagnosticKind::CurlyChild.hint().to_string()),
+ vec![LintFix {
+ description: "Remove curly braces around JSX child".into(),
+ changes: vec![LintFixChange {
+ new_text: lit_str.value().to_string().into(),
+ range: child.range(),
+ }],
+ }],
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn jsx_attr(&mut self, node: &JSXAttr, ctx: &mut Context) {
+ if let Some(value) = node.value {
+ match value {
+ JSXAttrValue::JSXExprContainer(expr) => {
+ if let JSXExpr::Expr(expr) = expr.expr {
+ if let Expr::Lit(lit) = expr {
+ if let Lit::Str(lit_str) = lit {
+ ctx.add_diagnostic_with_fixes(
+ value.range(),
+ CODE,
+ DiagnosticKind::CurlyAttribute.message(),
+ Some(DiagnosticKind::CurlyAttribute.hint().to_string()),
+ vec![LintFix {
+ description:
+ "Remove curly braces around JSX attribute value".into(),
+ changes: vec![LintFixChange {
+ new_text: format!("\"{}\"", lit_str.value()).into(),
+ range: value.range(),
+ }],
+ }],
+ );
+ }
+ }
+ }
+ }
+ JSXAttrValue::JSXElement(jsx_el) => {
+ ctx.add_diagnostic_with_fixes(
+ value.range(),
+ CODE,
+ DiagnosticKind::MissingCurlyAttribute.message(),
+ Some(DiagnosticKind::MissingCurlyAttribute.hint().to_string()),
+ vec![LintFix {
+ description: "Add curly braces around JSX attribute value".into(),
+ changes: vec![LintFixChange {
+ new_text: format!("{{{}}}", jsx_el.text()).into(),
+ range: value.range(),
+ }],
+ }],
+ );
+ }
+ _ => {}
+ }
+ }
+ }
+}
+
+// most tests are taken from ESlint, commenting those
+// requiring code path support
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn jsx_curly_braces_valid() {
+ assert_lint_ok! {
+ JSXCurlyBraces,
+ filename: "file:///foo.jsx",
+ "",
+ };
+ }
+
+ #[test]
+ fn jsx_curly_braces_invalid() {
+ assert_lint_err! {
+ JSXCurlyBraces,
+ filename: "file:///foo.jsx",
+ "": [
+ {
+ col: 9,
+ message: DiagnosticKind::CurlyAttribute.message(),
+ hint: DiagnosticKind::CurlyAttribute.hint(),
+ fix: (
+ "Remove curly braces around JSX attribute value",
+ ""
+ )
+ }
+ ],
+ " />": [
+ {
+ col: 9,
+ message: DiagnosticKind::MissingCurlyAttribute.message(),
+ hint: DiagnosticKind::MissingCurlyAttribute.hint(),
+ fix: (
+ "Add curly braces around JSX attribute value",
+ "} />"
+ )
+ }
+ ],
+ r#"{"foo"}
"#: [
+ {
+ col: 5,
+ message: DiagnosticKind::CurlyChild.message(),
+ hint: DiagnosticKind::CurlyChild.hint(),
+ fix: (
+ "Remove curly braces around JSX child",
+ "foo
"
+ )
+ }
+ ],
+ };
+ }
+}
diff --git a/www/static/docs.json b/www/static/docs.json
index 3361afd6..6f953005 100644
--- a/www/static/docs.json
+++ b/www/static/docs.json
@@ -111,6 +111,14 @@
"docs": "Require `for-in` loops to include an `if` statement\n\nLooping over objects with a `for-in` loop will include properties that are\ninherited through the prototype chain. This behavior can lead to unexpected\nitems in your for loop.\n\n### Invalid:\n\n```typescript\nfor (const key in obj) {\n foo(obj, key);\n}\n```\n\n### Valid:\n\n```typescript\nfor (const key in obj) {\n if (Object.hasOwn(obj, key)) {\n foo(obj, key);\n }\n}\n```\n\n```typescript\nfor (const key in obj) {\n if (!Object.hasOwn(obj, key)) {\n continue;\n }\n foo(obj, key);\n}\n```\n",
"tags": []
},
+ {
+ "code": "jsx-curly-braces",
+ "docs": "Ensure consistent use of curly braces around JSX expressions.\n\n### Invalid:\n\n```tsx\nconst foo = />;\nconst foo = ;\nconst foo = {\"foo\"}
;\n```\n\n### Valid:\n\n```tsx\nconst foo = } />;\nconst foo = ;\nconst foo = foo
;\n```\n",
+ "tags": [
+ "react",
+ "jsx"
+ ]
+ },
{
"code": "no-array-constructor",
"docs": "Enforce conventional usage of array construction\n\nArray construction is conventionally done via literal notation such as `[]` or\n`[1, 2, 3]`. Using the `new Array()` is discouraged as is `new Array(1, 2, 3)`.\nThere are two reasons for this. The first is that a single supplied argument\ndefines the array length, while multiple arguments instead populate the array of\nno fixed size. This confusion is avoided when pre-populated arrays are only\ncreated using literal notation. The second argument to avoiding the `Array`\nconstructor is that the `Array` global may be redefined.\n\nThe one exception to this rule is when creating a new array of fixed size, e.g.\n`new Array(6)`. This is the conventional way to create arrays of fixed length.\n\n### Invalid:\n\n```typescript\n// This is 4 elements, not a size 100 array of 3 elements\nconst a = new Array(100, 1, 2, 3);\n\nconst b = new Array(); // use [] instead\n```\n\n### Valid:\n\n```typescript\nconst a = new Array(100);\nconst b = [];\nconst c = [1, 2, 3];\n```\n",