Skip to content

[New] Add suggestions to no-unescaped-entities #3831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

### Added
* add type generation ([#3830][] @voxpelli)
* [`no-unescaped-entities`]: add suggestions ([#3831][] @StyleShit)

[#3831]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3831
[#3830]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3830

## [7.36.1] - 2024.09.12
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -368,7 +368,7 @@ module.exports = [
| [no-string-refs](docs/rules/no-string-refs.md) | Disallow using string references | ☑️ | | | | |
| [no-this-in-sfc](docs/rules/no-this-in-sfc.md) | Disallow `this` from being used in stateless functional components | | | | | |
| [no-typos](docs/rules/no-typos.md) | Disallow common typos | | | | | |
| [no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Disallow unescaped HTML entities from appearing in markup | ☑️ | | | | |
| [no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Disallow unescaped HTML entities from appearing in markup | ☑️ | | | 💡 | |
| [no-unknown-property](docs/rules/no-unknown-property.md) | Disallow usage of unknown DOM property | ☑️ | | 🔧 | | |
| [no-unsafe](docs/rules/no-unsafe.md) | Disallow usage of unsafe lifecycle methods | | ☑️ | | | |
| [no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Disallow creating unstable components inside components | | | | | |
2 changes: 2 additions & 0 deletions docs/rules/no-unescaped-entities.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

💼 This rule is enabled in the ☑️ `recommended` [config](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs).

💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

<!-- end auto-generated rule header -->

This rule prevents characters that you may have meant as JSX escape characters
22 changes: 22 additions & 0 deletions lib/rules/no-unescaped-entities.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ const docsUrl = require('../util/docsUrl');
const getSourceCode = require('../util/eslint').getSourceCode;
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
const getMessageData = require('../util/message');

// ------------------------------------------------------------------------------
// Rule Definition
@@ -34,11 +35,13 @@ const DEFAULTS = [{
const messages = {
unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.',
replaceWithAlt: 'Replace with `{{alt}}`.',
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
hasSuggestions: true,
docs: {
description: 'Disallow unescaped HTML entities from appearing in markup',
category: 'Possible Errors',
@@ -117,6 +120,25 @@ module.exports = {
entity: entities[j].char,
alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '),
},
suggest: entities[j].alternatives.map((alt) => Object.assign(
getMessageData('replaceWithAlt', messages.replaceWithAlt),
{
data: { alt },
fix(fixer) {
const lineToChange = i - node.loc.start.line;

const newText = node.raw.split('\n').map((line, idx) => {
if (idx === lineToChange) {
return line.slice(0, index) + alt + line.slice(index + 1);
}

return line;
}).join('\n');

return fixer.replaceText(node, newText);
},
}
)),
});
}
}
267 changes: 267 additions & 0 deletions tests/lib/rules/no-unescaped-entities.js
Original file line number Diff line number Diff line change
@@ -135,6 +135,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>&gt; default parser</div>;
}
});
`,
},
],
},
],
},
@@ -152,6 +165,21 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>first line is ok
so is second
and here are some bad entities: &gt;</div>
}
});
`,
},
],
},
],
},
@@ -167,14 +195,86 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '\'', alts: '`&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&apos;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: &apos;>> default parser</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&lsquo;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: &lsquo;>> default parser</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&#39;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: &#39;>> default parser</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&rsquo;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: &rsquo;>> default parser</div>;
}
});
`,
},
],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: '&gt;> default parser</div>;
}
});
`,
},
],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>Multiple errors: '>&gt; default parser</div>;
}
});
`,
},
],
},
],
},
@@ -190,6 +290,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '}', alts: '`&#125;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&#125;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>{"Unbalanced braces - default parser"}&#125;</div>;
}
});
`,
},
],
},
],
},
@@ -207,6 +320,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <>&gt; babel-eslint</>;
}
});
`,
},
],
},
],
},
@@ -225,6 +351,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`&gt;`' },
suggestions: [{
messageId: 'replaceWithAlt',
data: { alt: '&gt;' },
output: `
var Hello = createReactClass({
render: function() {
return <>first line is ok
so is second
and here are some bad entities: &gt;</>
}
});
`,
}],
},
],
},
@@ -240,6 +379,52 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '\'', alts: '`&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`' },
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&apos;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>&apos;</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&lsquo;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>&lsquo;</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&#39;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>&#39;</div>;
}
});
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&rsquo;' },
output: `
var Hello = createReactClass({
render: function() {
return <div>&rsquo;</div>;
}
});
`,
},
],
},
],
},
@@ -256,6 +441,17 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '}', alts: '`&#125;`' },
suggestions: [{
messageId: 'replaceWithAlt',
data: { alt: '&#125;' },
output: `
var Hello = createReactClass({
render: function() {
return <>{"Unbalanced braces - babel-eslint"}&#125;</>;
}
});
`,
}],
},
],
},
@@ -304,6 +500,17 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '&', alts: '`&amp;`' },
suggestions: [{
messageId: 'replaceWithAlt',
data: { alt: '&amp;' },
output: `
var Hello = createReactClass({
render: function() {
return <span>foo &amp; bar</span>;
}
});
`,
}],
},
],
options: [
@@ -327,12 +534,72 @@ ruleTester.run('no-unescaped-entities', rule, {
data: { entity: '"', alts: '`&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`' },
line: 2,
column: 30,
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&quot;' },
output: `
<script>window.foo = &quot;bar"</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&ldquo;' },
output: `
<script>window.foo = &ldquo;bar"</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&#34;' },
output: `
<script>window.foo = &#34;bar"</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&rdquo;' },
output: `
<script>window.foo = &rdquo;bar"</script>
`,
},
],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '"', alts: '`&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`' },
line: 2,
column: 34,
suggestions: [
{
messageId: 'replaceWithAlt',
data: { alt: '&quot;' },
output: `
<script>window.foo = "bar&quot;</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&ldquo;' },
output: `
<script>window.foo = "bar&ldquo;</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&#34;' },
output: `
<script>window.foo = "bar&#34;</script>
`,
},
{
messageId: 'replaceWithAlt',
data: { alt: '&rdquo;' },
output: `
<script>window.foo = "bar&rdquo;</script>
`,
},
],
},
],
}