Skip to content

Commit

Permalink
feat: add jsx_curly_braces rule (#1354)
Browse files Browse the repository at this point in the history
* feat: add jsx_curly_braces rule

* fix: clippy

* fix: update schemas

* add recommended

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
  • Loading branch information
marvinhagemeister and bartlomieju authored Nov 29, 2024
1 parent 83c0cea commit 91ea797
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 0 deletions.
17 changes: 17 additions & 0 deletions docs/rules/jsx_curly_braces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Ensure consistent use of curly braces around JSX expressions.

### Invalid:

```tsx
const foo = <Foo foo=<div /> />;
const foo = <Foo str={"foo"} />;
const foo = <div>{"foo"}</div>;
```

### Valid:

```tsx
const foo = <Foo foo={<div />} />;
const foo = <Foo str="foo" />;
const foo = <div>foo</div>;
```
1 change: 1 addition & 0 deletions schemas/rules.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"fresh-server-event-handlers",
"getter-return",
"guard-for-in",
"jsx-curly-braces",
"jsx-no-children-prop",
"jsx-no-duplicate-props",
"jsx-props-no-spread-multi",
Expand Down
2 changes: 2 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 jsx_no_children_prop;
pub mod jsx_no_duplicate_props;
pub mod jsx_props_no_spread_multi;
Expand Down Expand Up @@ -259,6 +260,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
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(jsx_no_children_prop::JSXNoChildrenProp),
Box::new(jsx_no_duplicate_props::JSXNoDuplicateProps),
Box::new(jsx_props_no_spread_multi::JSXPropsNoSpreadMulti),
Expand Down
195 changes: 195 additions & 0 deletions src/rules/jsx_curly_braces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// 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] {
&["recommended", "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::Lit(Lit::Str(lit_str))) = child_expr.expr {
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::Lit(Lit::Str(lit_str))) = expr.expr {
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",
"<div foo={2} />",
};
}

#[test]
fn jsx_curly_braces_invalid() {
assert_lint_err! {
JSXCurlyBraces,
filename: "file:///foo.jsx",
"<div foo={'foo'} />": [
{
col: 9,
message: DiagnosticKind::CurlyAttribute.message(),
hint: DiagnosticKind::CurlyAttribute.hint(),
fix: (
"Remove curly braces around JSX attribute value",
"<div foo=\"foo\" />"
)
}
],
"<div foo=<div /> />": [
{
col: 9,
message: DiagnosticKind::MissingCurlyAttribute.message(),
hint: DiagnosticKind::MissingCurlyAttribute.hint(),
fix: (
"Add curly braces around JSX attribute value",
"<div foo={<div />} />"
)
}
],
r#"<div>{"foo"}</div>"#: [
{
col: 5,
message: DiagnosticKind::CurlyChild.message(),
hint: DiagnosticKind::CurlyChild.hint(),
fix: (
"Remove curly braces around JSX child",
"<div>foo</div>"
)
}
],
};
}
}
9 changes: 9 additions & 0 deletions www/static/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@
"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 = <Foo foo=<div /> />;\nconst foo = <Foo str={\"foo\"} />;\nconst foo = <div>{\"foo\"}</div>;\n```\n\n### Valid:\n\n```tsx\nconst foo = <Foo foo={<div />} />;\nconst foo = <Foo str=\"foo\" />;\nconst foo = <div>foo</div>;\n```\n",
"tags": [
"recommended",
"react",
"jsx"
]
},
{
"code": "jsx-no-children-prop",
"docs": "Pass children as JSX children instead of as an attribute.\n\n### Invalid:\n\n```tsx\n<div children=\"foo\" />\n<div children={[<Foo />, <Bar />]} />\n```\n\n### Valid:\n\n```tsx\n<div>foo</div>\n<div><Foo /><Bar /></div>\n```\n",
Expand Down

0 comments on commit 91ea797

Please sign in to comment.