Skip to content

Commit

Permalink
[Fix] no-unknown-property: properly tag-restrict case-insensitive a…
Browse files Browse the repository at this point in the history
…ttributes
  • Loading branch information
ljharb committed Oct 8, 2022
1 parent 5783f5d commit 028457c
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`jsx-key`]: Ignore elements inside `React.Children.toArray()` ([#1591][] @silvenon)
* [`jsx-no-constructed-context-values`]: fix false positive for usage in non-components ([#3448][] @golopot)
* [`static-property-placement`]: warn on nonstatic expected-statics ([#2581][] @ljharb)
* [`no-unknown-property`]: properly tag-restrict case-insensitive attributes (@ljharb)

### Changed
* [Docs] [`no-unknown-property`]: fix typo in link ([#3445][] @denkristoffer)
Expand Down
50 changes: 26 additions & 24 deletions lib/rules/no-unknown-property.js
Expand Up @@ -29,6 +29,7 @@ const DOM_ATTRIBUTE_NAMES = {

const ATTRIBUTE_TAGS_MAP = {
abbr: ['th', 'td'],
charset: ['meta'],
checked: ['input'],
// image is required for SVG support, all other tags are HTML.
crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
Expand Down Expand Up @@ -103,6 +104,9 @@ const ATTRIBUTE_TAGS_MAP = {
loop: ['audio', 'video'],
muted: ['audio', 'video'],
playsInline: ['video'],
allowFullScreen: ['video'],
webkitAllowFullScreen: ['video'],
mozAllowFullScreen: ['video'],
poster: ['video'],
preload: ['audio', 'video'],
scrolling: ['iframe'],
Expand Down Expand Up @@ -393,6 +397,22 @@ function isValidHTMLTagInJSX(childNode) {
return false;
}

/**
* Checks if the attribute name is included in the attributes that are excluded
* from the camel casing.
*
* // returns 'charSet'
* @example normalizeAttributeCase('charset')
*
* Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
*
* @param {String} name - Attribute name to be normalized
* @returns {String} Result
*/
function normalizeAttributeCase(name) {
return DOM_PROPERTIES_IGNORE_CASE.find((element) => element.toLowerCase() === name.toLowerCase()) || name;
}

/**
* Checks if an attribute name is a valid `data-*` attribute:
* if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
Expand All @@ -418,23 +438,6 @@ function isValidAriaAttribute(name) {
return ARIA_PROPERTIES.some((element) => element === name);
}

/**
* 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.toLowerCase() === name.toLowerCase());
}

/**
* Extracts the tag name for the JSXAttribute
* @param {JSXAttribute} node - JSXAttribute being tested.
Expand Down Expand Up @@ -523,10 +526,11 @@ module.exports = {
return {
JSXAttribute(node) {
const ignoreNames = getIgnoreConfig();
const name = context.getSourceCode().getText(node.name);
if (ignoreNames.indexOf(name) >= 0) {
const actualName = context.getSourceCode().getText(node.name);
if (ignoreNames.indexOf(actualName) >= 0) {
return;
}
const name = normalizeAttributeCase(actualName);

// Ignore tags like <Foo.bar />
if (tagNameHasDot(node)) {
Expand All @@ -537,8 +541,6 @@ module.exports = {

if (isValidAriaAttribute(name)) { return; }

if (isCaseIgnoredAttribute(name)) { return; }

const tagName = getTagName(node);

if (tagName === 'fbt') { return; } // fbt nodes are bonkers, let's not go there
Expand All @@ -555,7 +557,7 @@ module.exports = {
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
node,
data: {
name,
name: actualName,
tagName,
allowedTags: allowedTags.join(', '),
},
Expand All @@ -581,7 +583,7 @@ module.exports = {
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
node,
data: {
name,
name: actualName,
standardName,
},
fix(fixer) {
Expand All @@ -595,7 +597,7 @@ module.exports = {
report(context, messages.unknownProp, 'unknownProp', {
node,
data: {
name,
name: actualName,
},
});
},
Expand Down
10 changes: 9 additions & 1 deletion tests/lib/rules/no-unknown-property.js
Expand Up @@ -67,7 +67,7 @@ ruleTester.run('no-unknown-property', rule, {
{ code: '<link onLoad={bar} onError={foo} />' },
{ code: '<link rel="preload" as="image" href="someHref" imageSrcSet="someImageSrcSet" imageSizes="someImageSizes" />' },
{ code: '<object onLoad={bar} />' },
{ code: '<div allowFullScreen webkitAllowFullScreen mozAllowFullScreen />' },
{ code: '<video allowFullScreen webkitAllowFullScreen mozAllowFullScreen />' },
{ code: '<table border="1" />' },
{ code: '<th abbr="abbr" />' },
{ code: '<td abbr="abbr" />' },
Expand Down Expand Up @@ -506,6 +506,14 @@ ruleTester.run('no-unknown-property', rule, {
allowedTags: 'video',
},
},
{
messageId: 'invalidPropOnTag',
data: {
name: 'allowFullScreen',
tagName: 'div',
allowedTags: 'video',
},
},
],
},
{
Expand Down

0 comments on commit 028457c

Please sign in to comment.