Skip to content

Commit 028457c

Browse files
committed
[Fix] no-unknown-property: properly tag-restrict case-insensitive attributes
1 parent 5783f5d commit 028457c

File tree

3 files changed

+36
-25
lines changed

3 files changed

+36
-25
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* [`jsx-key`]: Ignore elements inside `React.Children.toArray()` ([#1591][] @silvenon)
1212
* [`jsx-no-constructed-context-values`]: fix false positive for usage in non-components ([#3448][] @golopot)
1313
* [`static-property-placement`]: warn on nonstatic expected-statics ([#2581][] @ljharb)
14+
* [`no-unknown-property`]: properly tag-restrict case-insensitive attributes (@ljharb)
1415

1516
### Changed
1617
* [Docs] [`no-unknown-property`]: fix typo in link ([#3445][] @denkristoffer)

lib/rules/no-unknown-property.js

+26-24
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const DOM_ATTRIBUTE_NAMES = {
2929

3030
const ATTRIBUTE_TAGS_MAP = {
3131
abbr: ['th', 'td'],
32+
charset: ['meta'],
3233
checked: ['input'],
3334
// image is required for SVG support, all other tags are HTML.
3435
crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
@@ -103,6 +104,9 @@ const ATTRIBUTE_TAGS_MAP = {
103104
loop: ['audio', 'video'],
104105
muted: ['audio', 'video'],
105106
playsInline: ['video'],
107+
allowFullScreen: ['video'],
108+
webkitAllowFullScreen: ['video'],
109+
mozAllowFullScreen: ['video'],
106110
poster: ['video'],
107111
preload: ['audio', 'video'],
108112
scrolling: ['iframe'],
@@ -393,6 +397,22 @@ function isValidHTMLTagInJSX(childNode) {
393397
return false;
394398
}
395399

400+
/**
401+
* Checks if the attribute name is included in the attributes that are excluded
402+
* from the camel casing.
403+
*
404+
* // returns 'charSet'
405+
* @example normalizeAttributeCase('charset')
406+
*
407+
* Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
408+
*
409+
* @param {String} name - Attribute name to be normalized
410+
* @returns {String} Result
411+
*/
412+
function normalizeAttributeCase(name) {
413+
return DOM_PROPERTIES_IGNORE_CASE.find((element) => element.toLowerCase() === name.toLowerCase()) || name;
414+
}
415+
396416
/**
397417
* Checks if an attribute name is a valid `data-*` attribute:
398418
* if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
@@ -418,23 +438,6 @@ function isValidAriaAttribute(name) {
418438
return ARIA_PROPERTIES.some((element) => element === name);
419439
}
420440

421-
/**
422-
* Checks if the attribute name is included in the attributes that are excluded
423-
* from the camel casing.
424-
*
425-
* // returns true
426-
* @example isCaseIgnoredAttribute('charSet')
427-
*
428-
* Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
429-
*
430-
* @param {String} name - Attribute name to be tested
431-
* @returns {Boolean} Result
432-
*/
433-
434-
function isCaseIgnoredAttribute(name) {
435-
return DOM_PROPERTIES_IGNORE_CASE.some((element) => element.toLowerCase() === name.toLowerCase());
436-
}
437-
438441
/**
439442
* Extracts the tag name for the JSXAttribute
440443
* @param {JSXAttribute} node - JSXAttribute being tested.
@@ -523,10 +526,11 @@ module.exports = {
523526
return {
524527
JSXAttribute(node) {
525528
const ignoreNames = getIgnoreConfig();
526-
const name = context.getSourceCode().getText(node.name);
527-
if (ignoreNames.indexOf(name) >= 0) {
529+
const actualName = context.getSourceCode().getText(node.name);
530+
if (ignoreNames.indexOf(actualName) >= 0) {
528531
return;
529532
}
533+
const name = normalizeAttributeCase(actualName);
530534

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

538542
if (isValidAriaAttribute(name)) { return; }
539543

540-
if (isCaseIgnoredAttribute(name)) { return; }
541-
542544
const tagName = getTagName(node);
543545

544546
if (tagName === 'fbt') { return; } // fbt nodes are bonkers, let's not go there
@@ -555,7 +557,7 @@ module.exports = {
555557
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
556558
node,
557559
data: {
558-
name,
560+
name: actualName,
559561
tagName,
560562
allowedTags: allowedTags.join(', '),
561563
},
@@ -581,7 +583,7 @@ module.exports = {
581583
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
582584
node,
583585
data: {
584-
name,
586+
name: actualName,
585587
standardName,
586588
},
587589
fix(fixer) {
@@ -595,7 +597,7 @@ module.exports = {
595597
report(context, messages.unknownProp, 'unknownProp', {
596598
node,
597599
data: {
598-
name,
600+
name: actualName,
599601
},
600602
});
601603
},

tests/lib/rules/no-unknown-property.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ ruleTester.run('no-unknown-property', rule, {
6767
{ code: '<link onLoad={bar} onError={foo} />' },
6868
{ code: '<link rel="preload" as="image" href="someHref" imageSrcSet="someImageSrcSet" imageSizes="someImageSizes" />' },
6969
{ code: '<object onLoad={bar} />' },
70-
{ code: '<div allowFullScreen webkitAllowFullScreen mozAllowFullScreen />' },
70+
{ code: '<video allowFullScreen webkitAllowFullScreen mozAllowFullScreen />' },
7171
{ code: '<table border="1" />' },
7272
{ code: '<th abbr="abbr" />' },
7373
{ code: '<td abbr="abbr" />' },
@@ -506,6 +506,14 @@ ruleTester.run('no-unknown-property', rule, {
506506
allowedTags: 'video',
507507
},
508508
},
509+
{
510+
messageId: 'invalidPropOnTag',
511+
data: {
512+
name: 'allowFullScreen',
513+
tagName: 'div',
514+
allowedTags: 'video',
515+
},
516+
},
509517
],
510518
},
511519
{

0 commit comments

Comments
 (0)