Repository: jsx-eslint/eslint-plugin-react (js)

Issue:
`no-unknown-property` doesn't prevent use of unknown properties?
Today React gave me an ["unknown prop"](https://facebook.github.io/react/warnings/unknown-prop.html) warning:

![image](https://cloud.githubusercontent.com/assets/943476/17540030/c37499a6-5e78-11e6-83ba-9820d7a11874.png)
In this example, I gave my `div` element an `abc` attribute, which is obviously invalid HTML.

I was surprised that my linter didn't pick up on this. Looking at the documentation, the name and description of the `no-unknown-property` rule lead me to believe that this should be caught, but that doesn't appear to be the case.

So, I suppose this is a feature request: add a rule to catch invalid DOM properties.


Pull request description:
[Fix] `no-unknown-property`: properly recognise unknown HTML/DOM attributes
Closes #757 by...

- renaming an old error message (`unknownProp` to `unknownPropWithStandardName`), since it reports an unknown property that is close (one lower/higher case letter) to an allowed (standard) name
- adding a new error message (`unknownProp`) that is used when an unknown property (without a 
- listing all one word and multiple word valid HTML/DOM attributes, and `aria-*` attributes
- adding a function that checks if attribute name is a valid `data-*` attribute
- adding more unit test cases
- moving one test that should have failed, from valid to invalid tests
- updating rule documentation

What was not implemented in this PR:
- mapping element specific tags to `ATTRIBUTE_TAGS_MAP` (e.g. attributes `optimum`, `high` and `low` should be used only on `<meter>` element)

While implementing these changes, I felt like I cannot be sure that this PR catches all valid HTML/DOM attributes. There are some listing [on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes) and [on React documentation](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes), but I could still find attributes that weren't on these listing that I felt would be beneficial to be listed in this rule (for example, the Safari/Apple related `autoCorrect` and `autoSave`, and React related `ref` and `defaultValue`). So, I'm sure there will be cases that this PR has missed, and the community will find them if these changes are merged and published.

Feel free to tag some who knows HTML/DOM attributes well and can help with reviewing and possibly suggesting more new unit test cases.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d38bc17c..b44cd97f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,10 +8,15 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 ### Fixed
 * [`jsx-key`]: avoid a crash with optional chaining ([#3371][] @ljharb)
 * [`jsx-sort-props`]: avoid a crash with spread props ([#3376][] @ljharb)
+* [`no-unknown-property`]: properly recognize valid data- and aria- attributes ([#3377][] @sjarva)
+* [`no-unknown-property`]: properly recognize unknown HTML/DOM attributes ([#3377][] @sjarva)
 
 ### 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)
+* [Refactor] [`no-unknown-property`]: update DOM properties to include also one word properties ([#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/docs/rules/no-unknown-property.md b/docs/rules/no-unknown-property.md
index 0a38a1b8..108fa42a 100644
--- a/docs/rules/no-unknown-property.md
+++ b/docs/rules/no-unknown-property.md
@@ -4,7 +4,8 @@
 
 🔧 This rule is automatically fixable using the `--fix` [flag](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) on the command line.
 
-In JSX all DOM properties and attributes should be camelCased to be consistent with standard JavaScript style. This can be a possible source of error if you are used to writing plain HTML.
+In JSX most DOM properties and attributes should be camelCased to be consistent with standard JavaScript style. This can be a possible source of error if you are used to writing plain HTML.
+Only `data-*` and `aria-*` attributes are usings hyphens and lowercase letters in JSX.
 
 ## Rule Details
 
@@ -14,6 +15,10 @@ Examples of **incorrect** code for this rule:
 var React = require('react');
 
 var Hello = <div class="hello">Hello World</div>;
+var Alphabet = <div abc="something">Alphabet</div>;
+
+// Invalid aria-* attribute
+var IconButton = <div aria-foo="bar" />;
 ```
 
 Examples of **correct** code for this rule:
@@ -22,6 +27,23 @@ Examples of **correct** code for this rule:
 var React = require('react');
 
 var Hello = <div className="hello">Hello World</div>;
+var Button = <button disabled>Cannot click me</button>;
+var Img = <img src={catImage} alt="A cat sleeping on a keyboard" />;
+
+// aria-* attributes
+var IconButton = <button aria-label="Close" onClick={this.close}>{closeIcon}</button>;
+
+// data-* attributes
+var Data = <div data-index={12}>Some data</div>;
+
+// React components are ignored
+var MyComponent = <App class="foo-bar"/>;
+var AnotherComponent = <Foo.bar for="bar" />;
+
+// Custom web components are ignored
+var MyElem = <div class="foo" is="my-elem"></div>;
+var AtomPanel = <atom-panel class="foo"></atom-panel>;
+
 ```
 
 ## Rule Options
diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js
index 09cf5481..d8f7f1e3 100644
--- a/lib/rules/no-unknown-property.js
+++ b/lib/rules/no-unknown-property.js
@@ -116,30 +116,75 @@ const SVGDOM_ATTRIBUTE_NAMES = {
   'xml:space': 'xmlSpace',
 };
 
-const DOM_PROPERTY_NAMES = [
-  // Standard
-  'acceptCharset', 'accessKey', 'allowFullScreen', 'autoComplete', 'autoFocus', 'autoPlay',
-  'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
-  'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
-  'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
+const DOM_PROPERTY_NAMES_ONE_WORD = [
+  // Global attributes - can be used on any HTML/DOM element
+  // See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
+  'dir', 'draggable', 'hidden', 'id', 'lang', 'nonce', 'part', 'slot', 'style', 'title', 'translate',
+  // Element specific attributes
+  // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
+  // To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
+  'accept', 'action', 'allow', 'alt', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
+  'content', 'coords', 'csp', 'data', 'decoding', 'default', 'defer', 'disabled', 'form',
+  'headers', 'height', 'high', 'href', 'icon', 'importance', 'integrity', 'kind', 'label',
+  'language', 'loading', 'list', 'loop', 'low', 'max', 'media', 'method', 'min', 'multiple', 'muted',
+  'name', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'profile',
+  'rel', 'required', 'reversed', 'role', 'rows', 'sandbox', 'scope', 'selected', 'shape', 'size', 'sizes',
+  'span', 'src', 'start', 'step', 'target', 'type', 'value', 'width', 'wrap',
+  // React specific attributes
+  'ref',
+];
+
+const DOM_PROPERTY_NAMES_TWO_WORDS = [
+  // Global attributes - can be used on any HTML/DOM element
+  // See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
+  'accessKey', 'autoCapitalize', 'autoFocus', 'contentEditable', 'enterKeyHint', 'exportParts',
+  'inputMode', 'itemID', 'itemRef', 'itemProp', 'itemScope', 'itemType', 'spellCheck', 'tabIndex',
+  // Element specific attributes
+  // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
+  // To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
+  'acceptCharset', 'allowFullScreen', 'autoComplete', 'autoPlay', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
+  'colSpan', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
+  'frameBorder', 'hrefLang', 'httpEquiv', 'isMap', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
   'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',
   'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onCopy', 'onCompositionEnd', 'onCompositionStart',
   'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
   'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver',
   'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver',
-  'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onTransitionEnd', 'radioGroup', 'readOnly', 'rowSpan',
-  'spellCheck', 'srcDoc', 'srcLang', 'srcSet', 'tabIndex', 'useMap',
-  // Non standard
-  'autoCapitalize', 'autoCorrect',
-  'autoSave',
-  'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID',
+  'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
+  'rowSpan', 'srcDoc', 'srcLang', 'srcSet', 'useMap',
+  // Safari/Apple specific, no listing available
+  'autoCorrect', // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
+  'autoSave', // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
+  // React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
+  'className', 'dangerouslySetInnerHTML', 'defaultValue', 'htmlFor', 'onChange', 'suppressContentEditableWarning', 'suppressHydrationWarning',
+];
+
+const DOM_PROPERTIES_IGNORE_CASE = ['charset'];
+
+const ARIA_PROPERTIES = [
+  // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
+  // Global attributes
+  'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current',
+  'aria-describedby', 'aria-description', 'aria-details',
+  'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup',
+  'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live',
+  'aria-owns', 'aria-relevant', 'aria-roledescription',
+  // Widget attributes
+  'aria-autocomplete', 'aria-checked', 'aria-expanded', 'aria-level', 'aria-modal', 'aria-multiline', 'aria-multiselectable',
+  'aria-orientation', 'aria-placeholder', 'aria-pressed', 'aria-readonly', 'aria-required', 'aria-selected',
+  'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
+  // Relationship attributes
+  'aria-activedescendant', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan',
+  'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
 ];
+
 function getDOMPropertyNames(context) {
+  const ALL_DOM_PROPERTY_NAMES = DOM_PROPERTY_NAMES_TWO_WORDS.concat(DOM_PROPERTY_NAMES_ONE_WORD);
   // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
   if (!testReactVersion(context, '>= 16.1.0')) {
-    return ['allowTransparency'].concat(DOM_PROPERTY_NAMES);
+    return ['allowTransparency'].concat(ALL_DOM_PROPERTY_NAMES);
   }
-  return DOM_PROPERTY_NAMES;
+  return ALL_DOM_PROPERTY_NAMES;
 }
 
 // ------------------------------------------------------------------------------
@@ -147,24 +192,74 @@ 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 `<fake>`'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 an attribute name is a valid `data-*` attribute:
+ * if the name starts with "data-" and has some lowcase (a to z) words, separated but hyphens (-)
+ * (which is also called "kebab case" or "dash case"), then the attribute is valid data attribute.
+ *
+ * @param {String} name - Attribute name to be tested
+ * @returns {boolean} Result
+ */
+function isValidDataAttribute(name) {
+  const dataAttrConvention = /^data(-[a-z]*)*$/;
+  return !!dataAttrConvention.test(name);
+}
+
+/**
+ * Checks if an attribute name is a standard aria attribute by compering it to a list
+ * of standard aria property names
+ *
+ * @param {String} name - Attribute name to be tested
+ * @returns {Boolean} Result
+ */
+
+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 === name.toLowerCase());
+}
+
 /**
  * Extracts the tag name for the JSXAttribute
  * @param {JSXAttribute} node - JSXAttribute being tested.
@@ -215,7 +310,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,39 +358,68 @@ module.exports = {
           return;
         }
 
+        if (isValidDataAttribute(name)) { return; }
+
+        if (isValidAriaAttribute(name)) { return; }
+
+        if (isCaseIgnoredAttribute(name)) { return; }
+
         const tagName = getTagName(node);
 
-        // 1. Some attributes are allowed on some tags only.
-        const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
-        if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
-          report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
+        // Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
+        if (isValidHTMLTagInJSX(node)) {
+          // Some attributes are allowed on some tags only
+          const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
+          if (tagName && allowedTags) {
+            // Scenario 1A: Allowed attribute found where not supposed to, report it
+            if (allowedTags.indexOf(tagName) === -1) {
+              report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
+                node,
+                data: {
+                  name,
+                  tagName,
+                  allowedTags: allowedTags.join(', '),
+                },
+              });
+            }
+            // Scenario 1B: There are allowed attributes on allowed tags, no need to report it
+            return;
+          }
+
+          // Let's see if the attribute is a close version to some standard property name
+          const standardName = getStandardName(name, context);
+
+          const hasStandardNameButIsNotUsed = standardName && standardName !== name;
+          const usesStandardName = standardName && standardName === name;
+
+          if (usesStandardName) {
+            // Scenario 2A: The attribute name is the standard name, no need to report it
+            return;
+          }
+
+          if (hasStandardNameButIsNotUsed) {
+            // Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
+            report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
+              node,
+              data: {
+                name,
+                standardName,
+              },
+              fix(fixer) {
+                return fixer.replaceText(node.name, standardName);
+              },
+            });
+            return;
+          }
+
+          // Scenario 3: We have an attribute that is unknown, report it
+          report(context, messages.unknownProp, 'unknownProp', {
             node,
             data: {
               name,
-              tagName,
-              allowedTags: allowedTags.join(', '),
             },
           });
         }
-
-        // 2. Otherwise, we'll try to find if the attribute is a close version
-        // of what we should normally have with React. If yes, we'll report an
-        // 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) {
-          return;
-        }
-        report(context, messages.unknownProp, 'unknownProp', {
-          node,
-          data: {
-            name,
-            standardName,
-          },
-          fix(fixer) {
-            return fixer.replaceText(node.name, standardName);
-          },
-        });
       },
     };
   },


Test patch:
diff --git a/tests/lib/rules/no-unknown-property.js b/tests/lib/rules/no-unknown-property.js
index 5191401f..0db6f3bc 100644
--- a/tests/lib/rules/no-unknown-property.js
+++ b/tests/lib/rules/no-unknown-property.js
@@ -29,39 +29,107 @@ const parserOptions = {
 const ruleTester = new RuleTester({ parserOptions });
 ruleTester.run('no-unknown-property', rule, {
   valid: parsers.all([
+    // React components and their props/attributes should be fine
     { code: '<App class="bar" />;' },
     { code: '<App for="bar" />;' },
+    { code: '<App someProp="bar" />;' },
     { code: '<Foo.bar for="bar" />;' },
     { code: '<App accept-charset="bar" />;' },
-    { code: '<meta charset="utf-8" />;' },
-    { code: '<meta charSet="utf-8" />;' },
     { code: '<App http-equiv="bar" />;' },
     {
       code: '<App xlink:href="bar" />;',
       features: ['jsx namespace'],
     },
     { code: '<App clip-path="bar" />;' },
+    // Some HTML/DOM elements with common attributes should work
     { code: '<div className="bar"></div>;' },
     { code: '<div onMouseDown={this._onMouseDown}></div>;' },
-    { code: '<div data-foo="bar"></div>;' },
+    { code: '<a href="someLink">Read more</a>' },
+    { code: '<img src="cat_keyboard.jpeg" alt="A cat sleeping on a keyboard" />' },
+    { code: '<input type="password" required />' },
+    { code: '<input ref={this.input} type="radio" />' },
+    { code: '<button disabled>You cannot click me</button>;' },
+    // Case ignored attributes, for `charset` discussion see https://github.com/jsx-eslint/eslint-plugin-react/pull/1863
+    { code: '<meta charset="utf-8" />;' },
+    { code: '<meta charSet="utf-8" />;' },
+    // Some custom web components that are allowed to use `class` instead of `className`
     { code: '<div class="foo" is="my-elem"></div>;' },
     { code: '<div {...this.props} class="foo" is="my-elem"></div>;' },
-    { code: '<atom-panel class="foo"></atom-panel>;' }, {
+    { code: '<atom-panel class="foo"></atom-panel>;' },
+    // data-* attributes should work
+    { code: '<div data-foo="bar"></div>;' },
+    { code: '<div data-foo-bar="baz"></div>;' },
+    { code: '<div data-parent="parent"></div>;' },
+    { code: '<div data-index-number="1234"></div>;' },
+    // Ignoring should work
+    {
       code: '<div class="bar"></div>;',
       options: [{ ignore: ['class'] }],
     },
+    {
+      code: '<div someProp="bar"></div>;',
+      options: [{ ignore: ['someProp'] }],
+    },
+
+    // aria-* attributes should work
+    { code: '<button aria-haspopup="true">Click me to open pop up</button>;' },
+    { code: '<button aria-label="Close" onClick={someThing.close} />;' },
+    // Attributes on allowed elements should work
     { code: '<script crossOrigin />' },
     { code: '<audio crossOrigin />' },
-    { code: '<div hasOwnProperty="should not be allowed tag" />' },
     { code: '<svg><image crossOrigin /></svg>' },
   ]),
   invalid: parsers.all([
+    {
+      code: '<div hasOwnProperty="should not be allowed property"></div>;',
+      errors: [
+        {
+          messageId: 'unknownProp',
+          data: {
+            name: 'hasOwnProperty',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div abc="should not be allowed property"></div>;',
+      errors: [
+        {
+          messageId: 'unknownProp',
+          data: {
+            name: 'abc',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div aria-fake="should not be allowed property"></div>;',
+      errors: [
+        {
+          messageId: 'unknownProp',
+          data: {
+            name: 'aria-fake',
+          },
+        },
+      ],
+    },
+    {
+      code: '<div someProp="bar"></div>;',
+      errors: [
+        {
+          messageId: 'unknownProp',
+          data: {
+            name: 'someProp',
+          },
+        },
+      ],
+    },
     {
       code: '<div class="bar"></div>;',
       output: '<div className="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'class',
             standardName: 'className',
@@ -74,7 +142,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div htmlFor="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'for',
             standardName: 'htmlFor',
@@ -87,7 +155,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div acceptCharset="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'accept-charset',
             standardName: 'acceptCharset',
@@ -100,7 +168,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div httpEquiv="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'http-equiv',
             standardName: 'httpEquiv',
@@ -113,7 +181,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div accessKey="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'accesskey',
             standardName: 'accessKey',
@@ -126,7 +194,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div onClick="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'onclick',
             standardName: 'onClick',
@@ -139,7 +207,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div onMouseDown="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'onmousedown',
             standardName: 'onMouseDown',
@@ -152,7 +220,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<div onMouseDown="bar"></div>;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'onMousedown',
             standardName: 'onMouseDown',
@@ -166,7 +234,7 @@ ruleTester.run('no-unknown-property', rule, {
       features: ['jsx namespace'],
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'xlink:href',
             standardName: 'xlinkHref',
@@ -179,7 +247,7 @@ ruleTester.run('no-unknown-property', rule, {
       output: '<rect clipPath="bar" />;',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'clip-path',
             standardName: 'clipPath',
@@ -191,7 +259,7 @@ ruleTester.run('no-unknown-property', rule, {
       code: '<script crossorigin />',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'crossorigin',
             standardName: 'crossOrigin',
@@ -204,7 +272,7 @@ ruleTester.run('no-unknown-property', rule, {
       code: '<div crossorigin />',
       errors: [
         {
-          messageId: 'unknownProp',
+          messageId: 'unknownPropWithStandardName',
           data: {
             name: 'crossorigin',
             standardName: 'crossOrigin',