feat(linter): eslint-plugin-react(no-unescaped-entities) (#1044)

This commit is contained in:
Cameron 2023-10-24 09:41:44 +01:00 committed by GitHub
parent 3eb28e4972
commit 64988f484b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 238 additions and 0 deletions

View file

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

View file

@ -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
/// <div> > </div>
/// ```
///
/// Correct
///
/// ```jsx
/// <div> &gt; </div>
/// ```
///
/// ```jsx
/// <div> {'>'} </div>
/// ```
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<char, &'static [&'static str]> = phf_map! {
'>' => &["&gt;"],
'"' => &["&quot;", "&ldquo;", "&#34;", "&rdquo;"],
'\'' => &["&apos;", "&lsquo;", "&#39;", "&rsquo;"],
'}' => &["&#125;"],
};
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
"
var Hello = createReactClass({
render: function() {
return (
<div/>
);
}
});
",
"
var Hello = createReactClass({
render: function() {
return <div>Here is some text!</div>;
}
});
",
"
var Hello = createReactClass({
render: function() {
return <div>I&rsquo;ve escaped some entities: &gt; &lt; &amp;</div>;
}
});
",
"
var Hello = createReactClass({
render: function() {
return <div>first line is ok
so is second
and here are some escaped entities: &gt; &lt; &amp;</div>;
}
});
",
"
var Hello = createReactClass({
render: function() {
return <div>{\">\" + \"<\" + \"&\" + '\"'}</div>;
},
});
",
"
var Hello = createReactClass({
render: function() {
return <>Here is some text!</>;
}
});
",
"
var Hello = createReactClass({
render: function() {
return <>I&rsquo;ve escaped some entities: &gt; &lt; &amp;</>;
}
});
",
"
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 <div>'</div>;
}
});
",
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 <span>foo & bar</span>;
// }
// });
// ",
r#"<script>window.foo = "bar"</script>"#,
];
Tester::new_without_config(NoUnescapedEntities::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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 &gt;
╭─[no_unescaped_entities.tsx:2:1]
2 │ render: function() {
3 │ return <>> babel-eslint</>;
· ─
4 │ }
╰────
⚠ eslint-plugin-react(no-unescaped-entities): `>` can be escaped with &gt;
╭─[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 &apos; or &lsquo; or &#39; or &rsquo;
╭─[no_unescaped_entities.tsx:3:1]
3 │ render: function() {
4 │ return <div>'</div>;
· ─
5 │ }
╰────
⚠ eslint-plugin-react(no-unescaped-entities): `}` can be escaped with &#125;
╭─[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 &quot; or &ldquo; or &#34; or &rdquo;
╭─[no_unescaped_entities.tsx:1:1]
1 │ <script>window.foo = "bar"</script>
· ─
╰────
⚠ eslint-plugin-react(no-unescaped-entities): `"` can be escaped with &quot; or &ldquo; or &#34; or &rdquo;
╭─[no_unescaped_entities.tsx:1:1]
1 │ <script>window.foo = "bar"</script>
· ─
╰────