no-unknown-property.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /**
  2. * @fileoverview Prevent usage of unknown DOM property
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const has = require('has');
  7. const docsUrl = require('../util/docsUrl');
  8. const versionUtil = require('../util/version');
  9. // ------------------------------------------------------------------------------
  10. // Constants
  11. // ------------------------------------------------------------------------------
  12. const DEFAULTS = {
  13. ignore: []
  14. };
  15. const DOM_ATTRIBUTE_NAMES = {
  16. 'accept-charset': 'acceptCharset',
  17. class: 'className',
  18. for: 'htmlFor',
  19. 'http-equiv': 'httpEquiv',
  20. crossorigin: 'crossOrigin'
  21. };
  22. const ATTRIBUTE_TAGS_MAP = {
  23. crossOrigin: ['script', 'img', 'video', 'audio', 'link']
  24. };
  25. const SVGDOM_ATTRIBUTE_NAMES = {
  26. 'accent-height': 'accentHeight',
  27. 'alignment-baseline': 'alignmentBaseline',
  28. 'arabic-form': 'arabicForm',
  29. 'baseline-shift': 'baselineShift',
  30. 'cap-height': 'capHeight',
  31. 'clip-path': 'clipPath',
  32. 'clip-rule': 'clipRule',
  33. 'color-interpolation': 'colorInterpolation',
  34. 'color-interpolation-filters': 'colorInterpolationFilters',
  35. 'color-profile': 'colorProfile',
  36. 'color-rendering': 'colorRendering',
  37. 'dominant-baseline': 'dominantBaseline',
  38. 'enable-background': 'enableBackground',
  39. 'fill-opacity': 'fillOpacity',
  40. 'fill-rule': 'fillRule',
  41. 'flood-color': 'floodColor',
  42. 'flood-opacity': 'floodOpacity',
  43. 'font-family': 'fontFamily',
  44. 'font-size': 'fontSize',
  45. 'font-size-adjust': 'fontSizeAdjust',
  46. 'font-stretch': 'fontStretch',
  47. 'font-style': 'fontStyle',
  48. 'font-variant': 'fontVariant',
  49. 'font-weight': 'fontWeight',
  50. 'glyph-name': 'glyphName',
  51. 'glyph-orientation-horizontal': 'glyphOrientationHorizontal',
  52. 'glyph-orientation-vertical': 'glyphOrientationVertical',
  53. 'horiz-adv-x': 'horizAdvX',
  54. 'horiz-origin-x': 'horizOriginX',
  55. 'image-rendering': 'imageRendering',
  56. 'letter-spacing': 'letterSpacing',
  57. 'lighting-color': 'lightingColor',
  58. 'marker-end': 'markerEnd',
  59. 'marker-mid': 'markerMid',
  60. 'marker-start': 'markerStart',
  61. 'overline-position': 'overlinePosition',
  62. 'overline-thickness': 'overlineThickness',
  63. 'paint-order': 'paintOrder',
  64. 'panose-1': 'panose1',
  65. 'pointer-events': 'pointerEvents',
  66. 'rendering-intent': 'renderingIntent',
  67. 'shape-rendering': 'shapeRendering',
  68. 'stop-color': 'stopColor',
  69. 'stop-opacity': 'stopOpacity',
  70. 'strikethrough-position': 'strikethroughPosition',
  71. 'strikethrough-thickness': 'strikethroughThickness',
  72. 'stroke-dasharray': 'strokeDasharray',
  73. 'stroke-dashoffset': 'strokeDashoffset',
  74. 'stroke-linecap': 'strokeLinecap',
  75. 'stroke-linejoin': 'strokeLinejoin',
  76. 'stroke-miterlimit': 'strokeMiterlimit',
  77. 'stroke-opacity': 'strokeOpacity',
  78. 'stroke-width': 'strokeWidth',
  79. 'text-anchor': 'textAnchor',
  80. 'text-decoration': 'textDecoration',
  81. 'text-rendering': 'textRendering',
  82. 'underline-position': 'underlinePosition',
  83. 'underline-thickness': 'underlineThickness',
  84. 'unicode-bidi': 'unicodeBidi',
  85. 'unicode-range': 'unicodeRange',
  86. 'units-per-em': 'unitsPerEm',
  87. 'v-alphabetic': 'vAlphabetic',
  88. 'v-hanging': 'vHanging',
  89. 'v-ideographic': 'vIdeographic',
  90. 'v-mathematical': 'vMathematical',
  91. 'vector-effect': 'vectorEffect',
  92. 'vert-adv-y': 'vertAdvY',
  93. 'vert-origin-x': 'vertOriginX',
  94. 'vert-origin-y': 'vertOriginY',
  95. 'word-spacing': 'wordSpacing',
  96. 'writing-mode': 'writingMode',
  97. 'x-height': 'xHeight',
  98. 'xlink:actuate': 'xlinkActuate',
  99. 'xlink:arcrole': 'xlinkArcrole',
  100. 'xlink:href': 'xlinkHref',
  101. 'xlink:role': 'xlinkRole',
  102. 'xlink:show': 'xlinkShow',
  103. 'xlink:title': 'xlinkTitle',
  104. 'xlink:type': 'xlinkType',
  105. 'xml:base': 'xmlBase',
  106. 'xml:lang': 'xmlLang',
  107. 'xml:space': 'xmlSpace'
  108. };
  109. const DOM_PROPERTY_NAMES = [
  110. // Standard
  111. 'acceptCharset', 'accessKey', 'allowFullScreen', 'autoComplete', 'autoFocus', 'autoPlay',
  112. 'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu',
  113. 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
  114. 'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
  115. 'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',
  116. 'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onCopy', 'onCompositionEnd', 'onCompositionStart',
  117. 'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
  118. 'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver',
  119. 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver',
  120. 'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onTransitionEnd', 'radioGroup', 'readOnly', 'rowSpan',
  121. 'spellCheck', 'srcDoc', 'srcLang', 'srcSet', 'tabIndex', 'useMap',
  122. // Non standard
  123. 'autoCapitalize', 'autoCorrect',
  124. 'autoSave',
  125. 'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID'
  126. ];
  127. function getDOMPropertyNames(context) {
  128. // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
  129. if (!versionUtil.testReactVersion(context, '16.1.0')) {
  130. return ['allowTransparency'].concat(DOM_PROPERTY_NAMES);
  131. }
  132. return DOM_PROPERTY_NAMES;
  133. }
  134. // ------------------------------------------------------------------------------
  135. // Helpers
  136. // ------------------------------------------------------------------------------
  137. /**
  138. * Checks if a node matches the JSX tag convention. This also checks if a node
  139. * is extended as a webcomponent using the attribute "is".
  140. * @param {Object} node - JSX element being tested.
  141. * @returns {boolean} Whether or not the node name match the JSX tag convention.
  142. */
  143. const tagConvention = /^[a-z][^-]*$/;
  144. function isTagName(node) {
  145. if (tagConvention.test(node.parent.name.name)) {
  146. // http://www.w3.org/TR/custom-elements/#type-extension-semantics
  147. return !node.parent.attributes.some((attrNode) => (
  148. attrNode.type === 'JSXAttribute'
  149. && attrNode.name.type === 'JSXIdentifier'
  150. && attrNode.name.name === 'is'
  151. ));
  152. }
  153. return false;
  154. }
  155. /**
  156. * Extracts the tag name for the JSXAttribute
  157. * @param {JSXAttribute} node - JSXAttribute being tested.
  158. * @returns {String|null} tag name
  159. */
  160. function getTagName(node) {
  161. if (node && node.parent && node.parent.name && node.parent.name) {
  162. return node.parent.name.name;
  163. }
  164. return null;
  165. }
  166. /**
  167. * Test wether the tag name for the JSXAttribute is
  168. * something like <Foo.bar />
  169. * @param {JSXAttribute} node - JSXAttribute being tested.
  170. * @returns {Boolean} result
  171. */
  172. function tagNameHasDot(node) {
  173. return !!(
  174. node.parent
  175. && node.parent.name
  176. && node.parent.name.type === 'JSXMemberExpression'
  177. );
  178. }
  179. /**
  180. * Get the standard name of the attribute.
  181. * @param {String} name - Name of the attribute.
  182. * @param {String} context - eslint context
  183. * @returns {String | undefined} The standard name of the attribute, or undefined if no standard name was found.
  184. */
  185. function getStandardName(name, context) {
  186. if (has(DOM_ATTRIBUTE_NAMES, name)) {
  187. return DOM_ATTRIBUTE_NAMES[name];
  188. }
  189. if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
  190. return SVGDOM_ATTRIBUTE_NAMES[name];
  191. }
  192. const names = getDOMPropertyNames(context);
  193. // Let's find a possible attribute match with a case-insensitive search.
  194. return names.find((element) => element.toLowerCase() === name.toLowerCase());
  195. }
  196. // ------------------------------------------------------------------------------
  197. // Rule Definition
  198. // ------------------------------------------------------------------------------
  199. module.exports = {
  200. meta: {
  201. docs: {
  202. description: 'Prevent usage of unknown DOM property',
  203. category: 'Possible Errors',
  204. recommended: true,
  205. url: docsUrl('no-unknown-property')
  206. },
  207. fixable: 'code',
  208. messages: {
  209. invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
  210. unknownProp: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead'
  211. },
  212. schema: [{
  213. type: 'object',
  214. properties: {
  215. ignore: {
  216. type: 'array',
  217. items: {
  218. type: 'string'
  219. }
  220. }
  221. },
  222. additionalProperties: false
  223. }]
  224. },
  225. create(context) {
  226. function getIgnoreConfig() {
  227. return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
  228. }
  229. return {
  230. JSXAttribute(node) {
  231. const ignoreNames = getIgnoreConfig();
  232. const name = context.getSourceCode().getText(node.name);
  233. if (ignoreNames.indexOf(name) >= 0) {
  234. return;
  235. }
  236. // Ignore tags like <Foo.bar />
  237. if (tagNameHasDot(node)) {
  238. return;
  239. }
  240. const tagName = getTagName(node);
  241. // 1. Some attributes are allowed on some tags only.
  242. const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
  243. if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
  244. context.report({
  245. node,
  246. messageId: 'invalidPropOnTag',
  247. data: {
  248. name,
  249. tagName,
  250. allowedTags: allowedTags.join(', ')
  251. }
  252. });
  253. }
  254. // 2. Otherwise, we'll try to find if the attribute is a close version
  255. // of what we should normally have with React. If yes, we'll report an
  256. // error. We don't want to report if the input attribute name is the
  257. // standard name though!
  258. const standardName = getStandardName(name, context);
  259. if (!isTagName(node) || !standardName || standardName === name) {
  260. return;
  261. }
  262. context.report({
  263. node,
  264. messageId: 'unknownProp',
  265. data: {
  266. name,
  267. standardName
  268. },
  269. fix(fixer) {
  270. return fixer.replaceText(node.name, standardName);
  271. }
  272. });
  273. }
  274. };
  275. }
  276. };