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: ` + + `, + }, + ], }, ], }