From d6d84ccb0620eeb2bfb1164afe431c9fa40085c2 Mon Sep 17 00:00:00 2001 From: Senja Jarva Date: Wed, 31 Aug 2022 16:39:43 +0300 Subject: [PATCH 1/4] [Refactor] `no-unknown-property`: improve jsdoc; extract logic to separate functions - checking case ignored attributes to a function of its own Also move variable inside function definition to make JSDoc comment match correct block Also update the link to Web components spec --- CHANGELOG.md | 4 +- lib/rules/no-unknown-property.js | 53 ++++++++++++++++++++------ tests/lib/rules/no-unknown-property.js | 24 ++++++------ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d38bc17c8a..310eded6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [`jsx-sort-props`]: avoid a crash with spread props ([#3376][] @ljharb) ### Changed -* [Docs] [`jsx-sort-propts`]: replace ref string with ref variable ([#3375][] @Luccasoli) +* [Docs] [`jsx-sort-props`]: replace ref string with ref variable ([#3375][] @Luccasoli) +* [Refactor] [`no-unknown-property`]: improve jsdoc; extract logic to separate functions ([#3377][] @sjarva) +[#3377]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3377 [#3376]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3376 [#3375]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3375 [#3371]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3371 diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index 09cf54818c..b9a3faae16 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -134,6 +134,9 @@ const DOM_PROPERTY_NAMES = [ 'autoSave', 'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID', ]; + +const DOM_PROPERTIES_IGNORE_CASE = ['charset']; + function getDOMPropertyNames(context) { // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823 if (!testReactVersion(context, '>= 16.1.0')) { @@ -147,24 +150,49 @@ function getDOMPropertyNames(context) { // ------------------------------------------------------------------------------ /** - * Checks if a node matches the JSX tag convention. This also checks if a node - * is extended as a webcomponent using the attribute "is". - * @param {Object} node - JSX element being tested. + * Checks if a node's parent is a JSX tag that is written with lowercase letters, + * and is not a custom web component. Custom web components have a hyphen in tag name, + * or have an `is="some-elem"` attribute. + * + * Note: does not check if a tag's parent against a list of standard HTML/DOM tags. For example, + * a ``'s child would return `true` because "fake" is written only with lowercase letters + * without a hyphen and does not have a `is="some-elem"` attribute. + * + * @param {Object} childNode - JSX element being tested. * @returns {boolean} Whether or not the node name match the JSX tag convention. */ -const tagConvention = /^[a-z][^-]*$/; -function isTagName(node) { - if (tagConvention.test(node.parent.name.name)) { - // https://www.w3.org/TR/custom-elements/#type-extension-semantics - return !node.parent.attributes.some((attrNode) => ( +function isValidHTMLTagInJSX(childNode) { + const tagConvention = /^[a-z][^-]*$/; + if (tagConvention.test(childNode.parent.name.name)) { + return !childNode.parent.attributes.some((attrNode) => ( attrNode.type === 'JSXAttribute' && attrNode.name.type === 'JSXIdentifier' && attrNode.name.name === 'is' + // To learn more about custom web components and `is` attribute, + // see https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-customized-builtin-example + )); } return false; } +/** + * Checks if the attribute name is included in the attributes that are excluded + * from the camel casing. + * + * // returns true + * @example isCaseIgnoredAttribute('charSet') + * + * Note - these exclusions are not made by React core team, but `eslint-plugin-react` community. + * + * @param {String} name - Attribute name to be tested + * @returns {Boolean} Result + */ + +function isCaseIgnoredAttribute(name) { + return DOM_PROPERTIES_IGNORE_CASE.some((element) => element === name.toLowerCase()); +} + /** * Extracts the tag name for the JSXAttribute * @param {JSXAttribute} node - JSXAttribute being tested. @@ -215,7 +243,8 @@ function getStandardName(name, context) { const messages = { invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}', - unknownProp: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead', + unknownPropWithStandardName: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead', + unknownProp: 'Unknown property \'{{name}}\' found', }; module.exports = { @@ -262,6 +291,8 @@ module.exports = { return; } + if (isCaseIgnoredAttribute(name)) { return; } + const tagName = getTagName(node); // 1. Some attributes are allowed on some tags only. @@ -282,10 +313,10 @@ module.exports = { // error. We don't want to report if the input attribute name is the // standard name though! const standardName = getStandardName(name, context); - if (!isTagName(node) || !standardName || standardName === name) { + if (!isValidHTMLTagInJSX(node) || !standardName || standardName === name) { return; } - report(context, messages.unknownProp, 'unknownProp', { + report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', { node, data: { name, diff --git a/tests/lib/rules/no-unknown-property.js b/tests/lib/rules/no-unknown-property.js index 5191401f34..920bfbf711 100644 --- a/tests/lib/rules/no-unknown-property.js +++ b/tests/lib/rules/no-unknown-property.js @@ -61,7 +61,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'class', standardName: 'className', @@ -74,7 +74,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'for', standardName: 'htmlFor', @@ -87,7 +87,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'accept-charset', standardName: 'acceptCharset', @@ -100,7 +100,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'http-equiv', standardName: 'httpEquiv', @@ -113,7 +113,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'accesskey', standardName: 'accessKey', @@ -126,7 +126,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'onclick', standardName: 'onClick', @@ -139,7 +139,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'onmousedown', standardName: 'onMouseDown', @@ -152,7 +152,7 @@ ruleTester.run('no-unknown-property', rule, { output: '
;', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'onMousedown', standardName: 'onMouseDown', @@ -166,7 +166,7 @@ ruleTester.run('no-unknown-property', rule, { features: ['jsx namespace'], errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'xlink:href', standardName: 'xlinkHref', @@ -179,7 +179,7 @@ ruleTester.run('no-unknown-property', rule, { output: ';', errors: [ { - messageId: 'unknownProp', + messageId: 'unknownPropWithStandardName', data: { name: 'clip-path', standardName: 'clipPath', @@ -191,7 +191,7 @@ ruleTester.run('no-unknown-property', rule, { code: '