diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4aad7555d7..a418675738 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`no-unknown-property`]: do not check `fbs` elements ([#3494][] @brianogilvie)
* [`jsx-newline`]: No newline between comments and jsx elements ([#3493][] @justmejulian)
* [`jsx-no-leaked-render`]: Don't report errors on empty strings if React >= v18 ([#3488][] @himanshu007-creator)
+* [`no-invalid-html-attribute`]: convert autofix to suggestion ([#3474][] @himanshu007-creator @ljharb)
### Changed
* [Docs] [`jsx-no-leaked-render`]: Remove mentions of empty strings for React 18 ([#3468][] @karlhorky)
@@ -24,6 +25,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
[#3494]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3494
[#3493]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3493
[#3488]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3488
+[#3474]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3474
[#3471]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3471
[#3468]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3468
[#3461]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3461
diff --git a/README.md b/README.md
index ccdcb2bc0d..9bb7d2c9e8 100644
--- a/README.md
+++ b/README.md
@@ -352,7 +352,7 @@ module.exports = [
| [no-did-update-set-state](docs/rules/no-did-update-set-state.md) | Disallow usage of setState in componentDidUpdate | | | | |
| [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md) | Disallow direct mutation of this.state | πΌ | | | |
| [no-find-dom-node](docs/rules/no-find-dom-node.md) | Disallow usage of findDOMNode | πΌ | | | |
-| [no-invalid-html-attribute](docs/rules/no-invalid-html-attribute.md) | Disallow usage of invalid attributes | | π§ | | |
+| [no-invalid-html-attribute](docs/rules/no-invalid-html-attribute.md) | Disallow usage of invalid attributes | | | π‘ | |
| [no-is-mounted](docs/rules/no-is-mounted.md) | Disallow usage of isMounted | πΌ | | | |
| [no-multi-comp](docs/rules/no-multi-comp.md) | Disallow multiple component definition per file | | | | |
| [no-namespace](docs/rules/no-namespace.md) | Enforce that namespaces are not used in React elements | | | | |
diff --git a/docs/rules/no-invalid-html-attribute.md b/docs/rules/no-invalid-html-attribute.md
index 12614ff900..d749ead037 100644
--- a/docs/rules/no-invalid-html-attribute.md
+++ b/docs/rules/no-invalid-html-attribute.md
@@ -1,6 +1,6 @@
# Disallow usage of invalid attributes (`react/no-invalid-html-attribute`)
-π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+π‘ This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
diff --git a/lib/rules/hook-use-state.js b/lib/rules/hook-use-state.js
index ba0785495c..a9deed4fd6 100644
--- a/lib/rules/hook-use-state.js
+++ b/lib/rules/hook-use-state.js
@@ -114,16 +114,12 @@ module.exports = {
getMessageData('suggestPair', messages.suggestPair),
{
fix(fixer) {
- if (expectedSetterVariableNames.length === 0) {
- return;
+ if (expectedSetterVariableNames.length > 0) {
+ return fixer.replaceTextRange(
+ node.parent.id.range,
+ `[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
+ );
}
-
- const fix = fixer.replaceTextRange(
- node.parent.id.range,
- `[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
- );
-
- return fix;
},
}
),
diff --git a/lib/rules/no-invalid-html-attribute.js b/lib/rules/no-invalid-html-attribute.js
index 9ba7bfa163..997afc2c81 100644
--- a/lib/rules/no-invalid-html-attribute.js
+++ b/lib/rules/no-invalid-html-attribute.js
@@ -8,6 +8,7 @@
const matchAll = require('string.prototype.matchall');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
+const getMessageData = require('../util/message');
// ------------------------------------------------------------------------------
// Rule Definition
@@ -232,6 +233,11 @@ const messages = {
onlyMeaningfulFor: 'The β{{attributeName}}β attribute only has meaning on the tags: {{tagNames}}',
onlyStrings: 'β{{attributeName}}β attribute only supports strings.',
spaceDelimited: 'β{{attributeName}}β attribute values should be space delimited.',
+ suggestRemoveDefault: '"remove {{attributeName}}"',
+ suggestRemoveEmpty: '"remove empty attribute {{attributeName}}"',
+ suggestRemoveInvalid: 'βremove invalid attribute {{reportingValue}}β',
+ suggestRemoveWhitespaces: 'remove whitespaces in β{{reportingValue}}β',
+ suggestRemoveNonString: 'remove non-string value in β{{reportingValue}}β',
};
function splitIntoRangedParts(node, regex) {
@@ -254,9 +260,12 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
report(context, messages.onlyStrings, 'onlyStrings', {
node,
data: { attributeName },
- fix(fixer) {
- return fixer.remove(parentNode);
- },
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveNonString', messages.suggestRemoveNonString),
+ { fix(fixer) { return fixer.remove(parentNode); } }
+ ),
+ ],
});
return;
}
@@ -265,9 +274,12 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
report(context, messages.noEmpty, 'noEmpty', {
node,
data: { attributeName },
- fix(fixer) {
- return fixer.remove(parentNode);
- },
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveEmpty', messages.suggestRemoveEmpty),
+ { fix(fixer) { return fixer.remove(node.parent); } }
+ ),
+ ],
});
return;
}
@@ -276,16 +288,23 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
for (const singlePart of singleAttributeParts) {
const allowedTags = VALID_VALUES.get(attributeName).get(singlePart.value);
const reportingValue = singlePart.reportingValue;
+
+ const suggest = [
+ Object.assign(
+ getMessageData('suggestRemoveInvalid', messages.suggestRemoveInvalid),
+ { fix(fixer) { return fixer.removeRange(singlePart.range); } }
+ ),
+ ];
+
if (!allowedTags) {
+ const data = {
+ attributeName,
+ reportingValue,
+ };
report(context, messages.neverValid, 'neverValid', {
node,
- data: {
- attributeName,
- reportingValue,
- },
- fix(fixer) {
- return fixer.removeRange(singlePart.range);
- },
+ data,
+ suggest,
});
} else if (!allowedTags.has(parentNodeName)) {
report(context, messages.notValidFor, 'notValidFor', {
@@ -295,9 +314,7 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
reportingValue,
elementName: parentNodeName,
},
- fix(fixer) {
- return fixer.removeRange(singlePart.range);
- },
+ suggest,
});
}
}
@@ -324,6 +341,7 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
secondValue,
missingValue: Array.from(siblings).join(', '),
},
+ suggest: false,
});
}
}
@@ -337,17 +355,23 @@ function checkLiteralValueNode(context, attributeName, node, parentNode, parentN
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
data: { attributeName },
- fix(fixer) {
- return fixer.removeRange(whitespacePart.range);
- },
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveWhitespaces', messages.suggestRemoveWhitespaces),
+ { fix(fixer) { return fixer.removeRange(whitespacePart.range); } }
+ ),
+ ],
});
} else if (whitespacePart.value !== '\u0020') {
report(context, messages.spaceDelimited, 'spaceDelimited', {
node,
data: { attributeName },
- fix(fixer) {
- return fixer.replaceTextRange(whitespacePart.range, '\u0020');
- },
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveWhitespaces', messages.suggestRemoveWhitespaces),
+ { fix(fixer) { return fixer.replaceTextRange(whitespacePart.range, '\u0020'); } }
+ ),
+ ],
});
}
}
@@ -358,10 +382,6 @@ const DEFAULT_ATTRIBUTES = ['rel'];
function checkAttribute(context, node) {
const attribute = node.name.name;
- function fix(fixer) {
- return fixer.remove(node);
- }
-
const parentNodeName = node.parent.name.name;
if (!COMPONENT_ATTRIBUTE_MAP.has(attribute) || !COMPONENT_ATTRIBUTE_MAP.get(attribute).has(parentNodeName)) {
const tagNames = Array.from(
@@ -374,16 +394,28 @@ function checkAttribute(context, node) {
attributeName: attribute,
tagNames,
},
- fix,
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
+ { fix(fixer) { return fixer.remove(node); } }
+ ),
+ ],
});
return;
}
+ function fix(fixer) { return fixer.remove(node); }
+
if (!node.value) {
report(context, messages.emptyIsMeaningless, 'emptyIsMeaningless', {
node,
data: { attributeName: attribute },
- fix,
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveEmpty', messages.suggestRemoveEmpty),
+ { fix }
+ ),
+ ],
});
return;
}
@@ -404,16 +436,23 @@ function checkAttribute(context, node) {
report(context, messages.onlyStrings, 'onlyStrings', {
node,
data: { attributeName: attribute },
- fix,
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
+ { fix }
+ ),
+ ],
});
- return;
- }
-
- if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
+ } else if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
report(context, messages.onlyStrings, 'onlyStrings', {
node,
data: { attributeName: attribute },
- fix,
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveDefault', messages.suggestRemoveDefault),
+ { fix }
+ ),
+ ],
});
}
}
@@ -441,11 +480,14 @@ function checkPropValidValue(context, node, value, attribute) {
attributeName: attribute,
reportingValue: value.value,
},
+ suggest: [
+ Object.assign(
+ getMessageData('suggestRemoveInvalid', messages.suggestRemoveInvalid),
+ { fix(fixer) { return fixer.replaceText(value, value.raw.replace(value.value, '')); } }
+ ),
+ ],
});
- return;
- }
-
- if (!validTagSet.has(node.arguments[0].value)) {
+ } else if (!validTagSet.has(node.arguments[0].value)) {
report(context, messages.notValidFor, 'notValidFor', {
node: value,
data: {
@@ -453,6 +495,7 @@ function checkPropValidValue(context, node, value, attribute) {
reportingValue: value.raw,
elementName: node.arguments[0].value,
},
+ suggest: false,
});
}
}
@@ -493,6 +536,7 @@ function checkCreateProps(context, node, attribute) {
attributeName: attribute,
tagNames,
},
+ suggest: false,
});
// eslint-disable-next-line no-continue
@@ -505,6 +549,7 @@ function checkCreateProps(context, node, attribute) {
data: {
attributeName: attribute,
},
+ suggest: false,
});
// eslint-disable-next-line no-continue
@@ -531,7 +576,6 @@ function checkCreateProps(context, node, attribute) {
module.exports = {
meta: {
- fixable: 'code',
docs: {
description: 'Disallow usage of invalid attributes',
category: 'Possible Errors',
@@ -545,6 +589,8 @@ module.exports = {
enum: ['rel'],
},
}],
+ type: 'suggestion',
+ hasSuggestions: true, // eslint-disable-line eslint-plugin/require-meta-has-suggestions
},
create(context) {
diff --git a/tests/helpers/parsers.js b/tests/helpers/parsers.js
index 1403f114c0..1d05610c6d 100644
--- a/tests/helpers/parsers.js
+++ b/tests/helpers/parsers.js
@@ -174,6 +174,8 @@ const parsers = {
tsNew ? addComment(Object.assign({}, test, { parser: parsers['@TYPESCRIPT_ESLINT'] }), '@typescript-eslint/parser') : []
);
});
+
+ // console.log(require('util').inspect(t, { depth: null }));
return t;
},
};
diff --git a/tests/lib/rules/no-invalid-html-attribute.js b/tests/lib/rules/no-invalid-html-attribute.js
index 2ba5b16883..cde601ba31 100644
--- a/tests/lib/rules/no-invalid-html-attribute.js
+++ b/tests/lib/rules/no-invalid-html-attribute.js
@@ -11,6 +11,7 @@
const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/no-invalid-html-attribute');
+const parsers = require('../../helpers/parsers');
const parserOptions = {
ecmaVersion: 2018,
@@ -27,7 +28,7 @@ const parserOptions = {
const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('no-invalid-html-attribute', rule, {
- valid: [
+ valid: parsers.all([
{ code: '' },
{ code: 'React.createElement("a", { rel: "alternate" })' },
{ code: 'React.createElement("a", { rel: ["alternate"] })' },
@@ -232,11 +233,10 @@ ruleTester.run('no-invalid-html-attribute', rule, {
{
code: '',
},
- ],
- invalid: [
+ ]),
+ invalid: parsers.all([
{
code: '',
- output: '',
errors: [
{
messageId: 'neverValid',
@@ -244,12 +244,17 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: '',
+ },
+ ],
},
],
},
{
code: 'React.createElement("a", { rel: "alternatex" })',
- output: null,
errors: [
{
messageId: 'neverValid',
@@ -257,12 +262,17 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: "" })',
+ },
+ ],
},
],
},
{
code: 'React.createElement("a", { rel: ["alternatex"] })',
- output: null,
errors: [
{
messageId: 'neverValid',
@@ -270,12 +280,17 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: [""] })',
+ },
+ ],
},
],
},
{
code: '',
- output: '',
errors: [
{
messageId: 'neverValid',
@@ -283,6 +298,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: '',
+ },
+ ],
},
],
},
@@ -295,6 +316,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex alternate',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: "" })',
+ },
+ ],
},
],
},
@@ -307,12 +334,17 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex alternate',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: [""] })',
+ },
+ ],
},
],
},
{
code: '',
- output: '',
errors: [
{
messageId: 'neverValid',
@@ -320,6 +352,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: '',
+ },
+ ],
},
],
},
@@ -332,6 +370,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternate alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: "" })',
+ },
+ ],
},
],
},
@@ -344,12 +388,17 @@ ruleTester.run('no-invalid-html-attribute', rule, {
reportingValue: 'alternate alternatex',
attributeName: 'rel',
},
+ suggestions: [
+ {
+ messageId: 'suggestRemoveInvalid',
+ output: 'React.createElement("a", { rel: [""] })',
+ },
+ ],
},
],
},
{
code: '',
- output: '',
errors: [
{
messageId: 'onlyMeaningfulFor',
@@ -357,6 +406,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
attributeName: 'rel',
tagNames: '"", "", "", "