Skip to content

Commit f54b391

Browse files
sjarvaljharb
authored andcommitted
[Fix] no-unknown-property: properly recognize unknown HTML/DOM attributes
Refactored going through different scenarios and error cases to be more clear. Also add some new unit tests, and move one should-have-been invalid test case from valid to invalid.
1 parent d2dd7f0 commit f54b391

File tree

4 files changed

+132
-34
lines changed

4 files changed

+132
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
* [`jsx-key`]: avoid a crash with optional chaining ([#3371][] @ljharb)
1010
* [`jsx-sort-props`]: avoid a crash with spread props ([#3376][] @ljharb)
1111
* [`no-unknown-property`]: properly recognize valid data- and aria- attributes ([#3377][] @sjarva)
12+
* [`no-unknown-property`]: properly recognize unknown HTML/DOM attributes ([#3377][] @sjarva)
1213

1314
### Changed
1415
* [Docs] [`jsx-sort-props`]: replace ref string with ref variable ([#3375][] @Luccasoli)

docs/rules/no-unknown-property.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Examples of **incorrect** code for this rule:
1515
var React = require('react');
1616

1717
var Hello = <div class="hello">Hello World</div>;
18+
var Alphabet = <div abc="something">Alphabet</div>;
19+
20+
// Invalid aria-* attribute
21+
var IconButton = <div aria-foo="bar" />;
1822
```
1923

2024
Examples of **correct** code for this rule:
@@ -23,14 +27,23 @@ Examples of **correct** code for this rule:
2327
var React = require('react');
2428

2529
var Hello = <div className="hello">Hello World</div>;
30+
var Button = <button disabled>Cannot click me</button>;
31+
var Img = <img src={catImage} alt="A cat sleeping on a keyboard" />;
2632

2733
// aria-* attributes
2834
var IconButton = <button aria-label="Close" onClick={this.close}>{closeIcon}</button>;
2935

30-
3136
// data-* attributes
3237
var Data = <div data-index={12}>Some data</div>;
3338

39+
// React components are ignored
40+
var MyComponent = <App class="foo-bar"/>;
41+
var AnotherComponent = <Foo.bar for="bar" />;
42+
43+
// Custom web components are ignored
44+
var MyElem = <div class="foo" is="my-elem"></div>;
45+
var AtomPanel = <atom-panel class="foo"></atom-panel>;
46+
3447
```
3548

3649
## Rule Options

lib/rules/no-unknown-property.js

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -366,37 +366,60 @@ module.exports = {
366366

367367
const tagName = getTagName(node);
368368

369-
// 1. Some attributes are allowed on some tags only.
370-
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
371-
if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
372-
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
369+
// Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
370+
if (isValidHTMLTagInJSX(node)) {
371+
// Some attributes are allowed on some tags only
372+
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
373+
if (tagName && allowedTags) {
374+
// Scenario 1A: Allowed attribute found where not supposed to, report it
375+
if (allowedTags.indexOf(tagName) === -1) {
376+
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
377+
node,
378+
data: {
379+
name,
380+
tagName,
381+
allowedTags: allowedTags.join(', '),
382+
},
383+
});
384+
}
385+
// Scenario 1B: There are allowed attributes on allowed tags, no need to report it
386+
return;
387+
}
388+
389+
// Let's see if the attribute is a close version to some standard property name
390+
const standardName = getStandardName(name, context);
391+
392+
const hasStandardNameButIsNotUsed = standardName && standardName !== name;
393+
const usesStandardName = standardName && standardName === name;
394+
395+
if (usesStandardName) {
396+
// Scenario 2A: The attribute name is the standard name, no need to report it
397+
return;
398+
}
399+
400+
if (hasStandardNameButIsNotUsed) {
401+
// Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
402+
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
403+
node,
404+
data: {
405+
name,
406+
standardName,
407+
},
408+
fix(fixer) {
409+
return fixer.replaceText(node.name, standardName);
410+
},
411+
});
412+
return;
413+
}
414+
415+
// Scenario 3: We have an attribute that is unknown, report it
416+
report(context, messages.unknownProp, 'unknownProp', {
373417
node,
374418
data: {
375419
name,
376-
tagName,
377-
allowedTags: allowedTags.join(', '),
378420
},
379421
});
380422
}
381-
382-
// 2. Otherwise, we'll try to find if the attribute is a close version
383-
// of what we should normally have with React. If yes, we'll report an
384-
// error. We don't want to report if the input attribute name is the
385-
// standard name though!
386-
const standardName = getStandardName(name, context);
387-
if (!isValidHTMLTagInJSX(node) || !standardName || standardName === name) {
388-
return;
389-
}
390-
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
391-
node,
392-
data: {
393-
name,
394-
standardName,
395-
},
396-
fix(fixer) {
397-
return fixer.replaceText(node.name, standardName);
398-
},
399-
});
400423
},
401424
};
402425
},

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

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,101 @@ const parserOptions = {
2929
const ruleTester = new RuleTester({ parserOptions });
3030
ruleTester.run('no-unknown-property', rule, {
3131
valid: parsers.all([
32+
// React components and their props/attributes should be fine
3233
{ code: '<App class="bar" />;' },
3334
{ code: '<App for="bar" />;' },
35+
{ code: '<App someProp="bar" />;' },
3436
{ code: '<Foo.bar for="bar" />;' },
3537
{ code: '<App accept-charset="bar" />;' },
36-
{ code: '<meta charset="utf-8" />;' },
37-
{ code: '<meta charSet="utf-8" />;' },
3838
{ code: '<App http-equiv="bar" />;' },
3939
{
4040
code: '<App xlink:href="bar" />;',
4141
features: ['jsx namespace'],
4242
},
4343
{ code: '<App clip-path="bar" />;' },
44+
// Some HTML/DOM elements with common attributes should work
4445
{ code: '<div className="bar"></div>;' },
4546
{ code: '<div onMouseDown={this._onMouseDown}></div>;' },
46-
// data attributes should work
47+
{ code: '<a href="someLink">Read more</a>' },
48+
{ code: '<img src="cat_keyboard.jpeg" alt="A cat sleeping on a keyboard" />' },
49+
{ code: '<input type="password" required />' },
50+
{ code: '<input ref={this.input} type="radio" />' },
51+
{ code: '<button disabled>You cannot click me</button>;' },
52+
// Case ignored attributes, for `charset` discussion see https://github.com/jsx-eslint/eslint-plugin-react/pull/1863
53+
{ code: '<meta charset="utf-8" />;' },
54+
{ code: '<meta charSet="utf-8" />;' },
55+
// Some custom web components that are allowed to use `class` instead of `className`
56+
{ code: '<div class="foo" is="my-elem"></div>;' },
57+
{ code: '<div {...this.props} class="foo" is="my-elem"></div>;' },
58+
{ code: '<atom-panel class="foo"></atom-panel>;' },
59+
// data-* attributes should work
4760
{ code: '<div data-foo="bar"></div>;' },
4861
{ code: '<div data-foo-bar="baz"></div>;' },
4962
{ code: '<div data-parent="parent"></div>;' },
5063
{ code: '<div data-index-number="1234"></div>;' },
51-
{ code: '<div class="foo" is="my-elem"></div>;' },
52-
{ code: '<div {...this.props} class="foo" is="my-elem"></div>;' },
53-
{ code: '<atom-panel class="foo"></atom-panel>;' }, {
64+
// Ignoring should work
65+
{
5466
code: '<div class="bar"></div>;',
5567
options: [{ ignore: ['class'] }],
5668
},
57-
// aria attributes should work
69+
{
70+
code: '<div someProp="bar"></div>;',
71+
options: [{ ignore: ['someProp'] }],
72+
},
73+
74+
// aria-* attributes should work
5875
{ code: '<button aria-haspopup="true">Click me to open pop up</button>;' },
5976
{ code: '<button aria-label="Close" onClick={someThing.close} />;' },
77+
// Attributes on allowed elements should work
6078
{ code: '<script crossOrigin />' },
6179
{ code: '<audio crossOrigin />' },
62-
{ code: '<div hasOwnProperty="should not be allowed tag" />' },
6380
{ code: '<svg><image crossOrigin /></svg>' },
6481
]),
6582
invalid: parsers.all([
83+
{
84+
code: '<div hasOwnProperty="should not be allowed property"></div>;',
85+
errors: [
86+
{
87+
messageId: 'unknownProp',
88+
data: {
89+
name: 'hasOwnProperty',
90+
},
91+
},
92+
],
93+
},
94+
{
95+
code: '<div abc="should not be allowed property"></div>;',
96+
errors: [
97+
{
98+
messageId: 'unknownProp',
99+
data: {
100+
name: 'abc',
101+
},
102+
},
103+
],
104+
},
105+
{
106+
code: '<div aria-fake="should not be allowed property"></div>;',
107+
errors: [
108+
{
109+
messageId: 'unknownProp',
110+
data: {
111+
name: 'aria-fake',
112+
},
113+
},
114+
],
115+
},
116+
{
117+
code: '<div someProp="bar"></div>;',
118+
errors: [
119+
{
120+
messageId: 'unknownProp',
121+
data: {
122+
name: 'someProp',
123+
},
124+
},
125+
],
126+
},
66127
{
67128
code: '<div class="bar"></div>;',
68129
output: '<div className="bar"></div>;',

0 commit comments

Comments
 (0)