diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 69470e820..548e71c54 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -132,6 +132,7 @@ mod react { pub mod no_children_prop; pub mod no_dangerously_set_inner_html; pub mod no_find_dom_node; + pub mod no_unescaped_entities; } mod unicorn { @@ -258,6 +259,7 @@ oxc_macros::declare_all_lint_rules! { react::jsx_key, react::jsx_no_comment_text_nodes, react::jsx_no_duplicate_props, + react::no_unescaped_entities, react::jsx_no_useless_fragment, react::no_children_prop, react::no_dangerously_set_inner_html, diff --git a/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs b/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs new file mode 100644 index 000000000..bac66deca --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_unescaped_entities.rs @@ -0,0 +1,187 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use phf::{phf_map, Map}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-react(no-unescaped-entities): `{1}` can be escaped with {2}")] +#[diagnostic(severity(warning))] +struct NoUnescapedEntitiesDiagnostic(#[label] pub Span, pub char, pub String); + +#[derive(Debug, Default, Clone)] +pub struct NoUnescapedEntities; + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule prevents characters that you may have meant as JSX escape characters from being accidentally injected as a text node in JSX statements. + /// + /// ### Why is this bad? + /// + /// JSX escape characters are used to inject characters into JSX statements that would otherwise be interpreted as code. + /// + /// ### Example + /// Incorrect + /// + /// ```jsx + ///
>
+ /// ``` + /// + /// Correct + /// + /// ```jsx + ///
>
+ /// ``` + /// + /// ```jsx + ///
{'>'}
+ /// ``` + NoUnescapedEntities, + correctness +); + +impl Rule for NoUnescapedEntities { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::JSXText(jsx_text) = node.kind() { + let source = jsx_text.span.source_text(ctx.source_text()); + + for (i, char) in source.chars().enumerate() { + if let Some(escapes) = DEFAULTS.get(&char) { + #[allow(clippy::cast_possible_truncation)] + ctx.diagnostic(NoUnescapedEntitiesDiagnostic( + Span { + start: jsx_text.span.start + i as u32, + end: jsx_text.span.start + i as u32 + 1, + }, + char, + escapes.join(" or "), + )); + } + } + } + } +} + +pub const DEFAULTS: Map = phf_map! { + '>' => &[">"], + '"' => &[""", "“", """, "”"], + '\'' => &["'", "‘", "'", "’"], + '}' => &["}"], +}; + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + " + var Hello = createReactClass({ + render: function() { + return ( +
+ ); + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return
Here is some text!
; + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return
I’ve escaped some entities: > < &
; + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return
first line is ok + so is second + and here are some escaped entities: > < &
; + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return
{\">\" + \"<\" + \"&\" + '\"'}
; + }, + }); + ", + " + var Hello = createReactClass({ + render: function() { + return <>Here is some text!; + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return <>I’ve escaped some entities: > < &; + } + }); + ", + " + var Hello = createReactClass({ + render: function() { + return <>{\">\" + \"<\" + \"&\" + '\"'}; + }, + }); + ", + ]; + + let fail = vec![ + "var Hello = createReactClass({ + render: function() { + return <>> babel-eslint; + } + });", + "var Hello = createReactClass({ + render: function() { + return <>first line is ok + so is second + and here are some bad entities: > + } + });", + " + var Hello = createReactClass({ + render: function() { + return
'
; + } + }); + ", + r#" + var Hello = createReactClass({ + render: function() { + return <>{"Unbalanced braces - babel-eslint"}}; + } + }); + "#, + // "var Hello = createReactClass({ + // render: function() { + // return <>foo & bar; + // } + // });", + // " var Hello = createReactClass({ + // render: function() { + // return foo & bar; + // } + // }); + // ", + r#""#, + ]; + + Tester::new_without_config(NoUnescapedEntities::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap b/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap new file mode 100644 index 000000000..93e33b37a --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_unescaped_entities.snap @@ -0,0 +1,49 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_unescaped_entities +--- + ⚠ eslint-plugin-react(no-unescaped-entities): `>` can be escaped with > + ╭─[no_unescaped_entities.tsx:2:1] + 2 │ render: function() { + 3 │ return <>> babel-eslint; + · ─ + 4 │ } + ╰──── + + ⚠ eslint-plugin-react(no-unescaped-entities): `>` can be escaped with > + ╭─[no_unescaped_entities.tsx:4:1] + 4 │ so is second + 5 │ and here are some bad entities: > + · ─ + 6 │ } + ╰──── + + ⚠ eslint-plugin-react(no-unescaped-entities): `'` can be escaped with ' or ‘ or ' or ’ + ╭─[no_unescaped_entities.tsx:3:1] + 3 │ render: function() { + 4 │ return
'
; + · ─ + 5 │ } + ╰──── + + ⚠ eslint-plugin-react(no-unescaped-entities): `}` can be escaped with } + ╭─[no_unescaped_entities.tsx:3:1] + 3 │ render: function() { + 4 │ return <>{"Unbalanced braces - babel-eslint"}}; + · ─ + 5 │ } + ╰──── + + ⚠ eslint-plugin-react(no-unescaped-entities): `"` can be escaped with " or “ or " or ” + ╭─[no_unescaped_entities.tsx:1:1] + 1 │ + · ─ + ╰──── + + ⚠ eslint-plugin-react(no-unescaped-entities): `"` can be escaped with " or “ or " or ” + ╭─[no_unescaped_entities.tsx:1:1] + 1 │ + · ─ + ╰──── + +