diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb4aa16c0d..9f24ca7dd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 3df80eb2da..37165b1459 100644
--- a/README.md
+++ b/README.md
@@ -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 | | | | | |
diff --git a/docs/rules/no-unescaped-entities.md b/docs/rules/no-unescaped-entities.md
index 2f6f8906b6..7146bc484c 100644
--- a/docs/rules/no-unescaped-entities.md
+++ b/docs/rules/no-unescaped-entities.md
@@ -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).
+
This rule prevents characters that you may have meant as JSX escape characters
diff --git a/lib/rules/no-unescaped-entities.js b/lib/rules/no-unescaped-entities.js
index ecf1abc5e1..3ec2cb23b6 100644
--- a/lib/rules/no-unescaped-entities.js
+++ b/lib/rules/no-unescaped-entities.js
@@ -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);
+ },
+ }
+ )),
});
}
}
diff --git a/tests/lib/rules/no-unescaped-entities.js b/tests/lib/rules/no-unescaped-entities.js
index 227365f5ea..4e62a82eb3 100644
--- a/tests/lib/rules/no-unescaped-entities.js
+++ b/tests/lib/rules/no-unescaped-entities.js
@@ -135,6 +135,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return
> default parser
;
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -152,6 +165,21 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return first line is ok
+ so is second
+ and here are some bad entities: >
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -167,14 +195,86 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '\'', alts: '`'`, `‘`, `'`, `’`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: ''' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: '>> default parser
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '‘' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: ‘>> default parser
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: ''' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: '>> default parser
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '’' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: ’>> default parser
;
+ }
+ });
+ `,
+ },
+ ],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: '>> default parser
;
+ }
+ });
+ `,
+ },
+ ],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return Multiple errors: '>> default parser
;
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -190,6 +290,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '}', alts: '`}`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '}' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return {"Unbalanced braces - default parser"}}
;
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -207,6 +320,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return <>> babel-eslint>;
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -225,6 +351,19 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '>', alts: '`>`' },
+ suggestions: [{
+ messageId: 'replaceWithAlt',
+ data: { alt: '>' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return <>first line is ok
+ so is second
+ and here are some bad entities: >>
+ }
+ });
+ `,
+ }],
},
],
},
@@ -240,6 +379,52 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '\'', alts: '`'`, `‘`, `'`, `’`' },
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: ''' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return '
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '‘' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return ‘
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: ''' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return '
;
+ }
+ });
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '’' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return ’
;
+ }
+ });
+ `,
+ },
+ ],
},
],
},
@@ -256,6 +441,17 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '}', alts: '`}`' },
+ suggestions: [{
+ messageId: 'replaceWithAlt',
+ data: { alt: '}' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return <>{"Unbalanced braces - babel-eslint"}}>;
+ }
+ });
+ `,
+ }],
},
],
},
@@ -304,6 +500,17 @@ ruleTester.run('no-unescaped-entities', rule, {
{
messageId: 'unescapedEntityAlts',
data: { entity: '&', alts: '`&`' },
+ suggestions: [{
+ messageId: 'replaceWithAlt',
+ data: { alt: '&' },
+ output: `
+ var Hello = createReactClass({
+ render: function() {
+ return foo & bar;
+ }
+ });
+ `,
+ }],
},
],
options: [
@@ -327,12 +534,72 @@ ruleTester.run('no-unescaped-entities', rule, {
data: { entity: '"', alts: '`"`, `“`, `"`, `”`' },
line: 2,
column: 30,
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '"' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '“' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '"' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '”' },
+ output: `
+
+ `,
+ },
+ ],
},
{
messageId: 'unescapedEntityAlts',
data: { entity: '"', alts: '`"`, `“`, `"`, `”`' },
line: 2,
column: 34,
+ suggestions: [
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '"' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '“' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '"' },
+ output: `
+
+ `,
+ },
+ {
+ messageId: 'replaceWithAlt',
+ data: { alt: '”' },
+ output: `
+
+ `,
+ },
+ ],
},
],
}