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",