Skip to content

Commit

Permalink
feat: add jsx-no-unescaped-entities rule
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Dec 10, 2024
1 parent 26e9df1 commit a2a2bea
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 0 deletions.
16 changes: 16 additions & 0 deletions docs/rules/jsx_no_unescaped_entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Using unescaped entities is often a coding mistake where the developer wanted to
pass a JSX element instead. This rule ensures an explicit text form must be
used.

### Invalid:

```tsx
<div>></div>;
```

### Valid:

```tsx
<div>&gt;</div>
<div>{">"}</div>
```
1 change: 1 addition & 0 deletions schemas/rules.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"jsx-no-comment-text-nodes",
"jsx-no-danger-with-children",
"jsx-no-duplicate-props",
"jsx-no-unescaped-entities",
"jsx-no-useless-fragment",
"jsx-props-no-spread-multi",
"jsx-void-dom-elements-no-children",
Expand Down
2 changes: 2 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub mod jsx_no_children_prop;
pub mod jsx_no_comment_text_nodes;
pub mod jsx_no_danger_with_children;
pub mod jsx_no_duplicate_props;
pub mod jsx_no_unescaped_entities;
pub mod jsx_no_useless_fragment;
pub mod jsx_props_no_spread_multi;
pub mod jsx_void_dom_elements_no_children;
Expand Down Expand Up @@ -279,6 +280,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
Box::new(jsx_no_comment_text_nodes::JSXNoCommentTextNodes),
Box::new(jsx_no_danger_with_children::JSXNoDangerWithChildren),
Box::new(jsx_no_duplicate_props::JSXNoDuplicateProps),
Box::new(jsx_no_unescaped_entities::JSXNoUnescapedEntities),
Box::new(jsx_no_useless_fragment::JSXNoUselessFragment),
Box::new(jsx_props_no_spread_multi::JSXPropsNoSpreadMulti),
Box::new(jsx_void_dom_elements_no_children::JSXVoidDomElementsNoChildren),
Expand Down
107 changes: 107 additions & 0 deletions src/rules/jsx_no_unescaped_entities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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::tags::{self, Tags};
use crate::Program;
use deno_ast::view::{JSXElement, JSXElementChild};
use deno_ast::SourceRanged;

#[derive(Debug)]
pub struct JSXNoUnescapedEntities;

const CODE: &str = "jsx-no-unescaped-entities";

impl LintRule for JSXNoUnescapedEntities {
fn tags(&self) -> Tags {
&[tags::RECOMMENDED, tags::REACT, tags::JSX, tags::FRESH]
}

fn code(&self) -> &'static str {
CODE
}

fn lint_program_with_ast_view(
&self,
context: &mut Context,
program: Program,
) {
JSXNoUnescapedEntitiesHandler.traverse(program, context);
}

#[cfg(feature = "docs")]
fn docs(&self) -> &'static str {
include_str!("../../docs/rules/jsx_no_unescaped_entities.md")
}
}

const MESSAGE: &str = "Found one or more unescaped entities in JSX text";
const HINT: &str = "Escape the '\">} characters respectively";

struct JSXNoUnescapedEntitiesHandler;

impl Handler for JSXNoUnescapedEntitiesHandler {
fn jsx_element(&mut self, node: &JSXElement, ctx: &mut Context) {
for child in node.children {
if let JSXElementChild::JSXText(jsx_text) = child {
let text = jsx_text.raw().as_str();
let new_text = text
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
.replace('}', "&#125;");

if text != new_text {
ctx.add_diagnostic_with_fixes(
node.range(),
CODE,
MESSAGE,
Some(HINT.to_string()),
vec![LintFix {
description: "Escape entities in the text node".into(),
changes: vec![LintFixChange {
new_text: new_text.into(),
range: child.range(),
}],
}],
);
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn jsx_no_unescaped_entities_valid() {
assert_lint_ok! {
JSXNoUnescapedEntities,
filename: "file:///foo.jsx",
r#"<div>&gt;</div>"#,
r#"<div>{">"}</div>"#,
};
}

#[test]
fn jsx_no_unescaped_entities_invalid() {
assert_lint_err! {
JSXNoUnescapedEntities,
filename: "file:///foo.jsx",
r#"<div>'">}</div>"#: [
{
col: 0,
message: MESSAGE,
hint: HINT,
fix: (
"Escape entities in the text node",
"<div>&apos;&quot;&gt;&#125;</div>"
)
}
]
};
}
}
10 changes: 10 additions & 0 deletions www/static/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@
"jsx"
]
},
{
"code": "jsx-no-unescaped-entities",
"docs": "Using unescaped entities is often a coding mistake where the developer wanted to\npass a JSX element instead. This rule ensures an explicit text form must be\nused.\n\n### Invalid:\n\n```tsx\n<div>></div>;\n```\n\n### Valid:\n\n```tsx\n<div>&gt;</div>\n<div>{\">\"}</div>\n```\n",
"tags": [
"recommended",
"react",
"jsx",
"fresh"
]
},
{
"code": "jsx-no-useless-fragment",
"docs": "Fragments are only necessary at the top of a JSX \"block\" and only when there are\nmultiple children. Fragments are not needed in other scenarios.\n\n### Invalid:\n\n```tsx\n<></>\n<><div /></>\n<><App /></>\n<p>foo <>bar</></p>\n```\n\n### Valid:\n\n```tsx\n<>{foo}</>\n<><div /><div /></>\n<>foo <div /></>\n<p>foo bar</p>\n```\n",
Expand Down

0 comments on commit a2a2bea

Please sign in to comment.