[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Prevent usage of unknown DOM property
|
---|
| 3 | * @author Yannick Croissant
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | 'use strict';
|
---|
| 7 |
|
---|
| 8 | const has = require('hasown');
|
---|
| 9 | const docsUrl = require('../util/docsUrl');
|
---|
| 10 | const getText = require('../util/eslint').getText;
|
---|
| 11 | const testReactVersion = require('../util/version').testReactVersion;
|
---|
| 12 | const report = require('../util/report');
|
---|
| 13 |
|
---|
| 14 | // ------------------------------------------------------------------------------
|
---|
| 15 | // Constants
|
---|
| 16 | // ------------------------------------------------------------------------------
|
---|
| 17 |
|
---|
| 18 | const DEFAULTS = {
|
---|
| 19 | ignore: [],
|
---|
| 20 | requireDataLowercase: false,
|
---|
| 21 | };
|
---|
| 22 |
|
---|
| 23 | const DOM_ATTRIBUTE_NAMES = {
|
---|
| 24 | 'accept-charset': 'acceptCharset',
|
---|
| 25 | class: 'className',
|
---|
| 26 | 'http-equiv': 'httpEquiv',
|
---|
| 27 | crossorigin: 'crossOrigin',
|
---|
| 28 | for: 'htmlFor',
|
---|
| 29 | nomodule: 'noModule',
|
---|
| 30 | };
|
---|
| 31 |
|
---|
| 32 | const ATTRIBUTE_TAGS_MAP = {
|
---|
| 33 | abbr: ['th', 'td'],
|
---|
| 34 | charset: ['meta'],
|
---|
| 35 | checked: ['input'],
|
---|
| 36 | // image is required for SVG support, all other tags are HTML.
|
---|
| 37 | crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
|
---|
| 38 | displaystyle: ['math'],
|
---|
| 39 | // https://html.spec.whatwg.org/multipage/links.html#downloading-resources
|
---|
| 40 | download: ['a', 'area'],
|
---|
| 41 | fill: [ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
|
---|
| 42 | // Fill color
|
---|
| 43 | 'altGlyph',
|
---|
| 44 | 'circle',
|
---|
| 45 | 'ellipse',
|
---|
| 46 | 'g',
|
---|
| 47 | 'line',
|
---|
| 48 | 'marker',
|
---|
| 49 | 'mask',
|
---|
| 50 | 'path',
|
---|
| 51 | 'polygon',
|
---|
| 52 | 'polyline',
|
---|
| 53 | 'rect',
|
---|
| 54 | 'svg',
|
---|
| 55 | 'symbol',
|
---|
| 56 | 'text',
|
---|
| 57 | 'textPath',
|
---|
| 58 | 'tref',
|
---|
| 59 | 'tspan',
|
---|
| 60 | 'use',
|
---|
| 61 | // Animation final state
|
---|
| 62 | 'animate',
|
---|
| 63 | 'animateColor',
|
---|
| 64 | 'animateMotion',
|
---|
| 65 | 'animateTransform',
|
---|
| 66 | 'set',
|
---|
| 67 | ],
|
---|
| 68 | focusable: ['svg'],
|
---|
| 69 | imageSizes: ['link'],
|
---|
| 70 | imageSrcSet: ['link'],
|
---|
| 71 | property: ['meta'],
|
---|
| 72 | viewBox: ['marker', 'pattern', 'svg', 'symbol', 'view'],
|
---|
| 73 | as: ['link'],
|
---|
| 74 | align: ['applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'], // deprecated, but known
|
---|
| 75 | valign: ['tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'colgroup', 'col'], // deprecated, but known
|
---|
| 76 | noModule: ['script'],
|
---|
| 77 | // Media events allowed only on audio and video tags, see https://github.com/facebook/react/blob/256aefbea1449869620fb26f6ec695536ab453f5/CHANGELOG.md#notable-enhancements
|
---|
| 78 | onAbort: ['audio', 'video'],
|
---|
| 79 | onCancel: ['dialog'],
|
---|
| 80 | onCanPlay: ['audio', 'video'],
|
---|
| 81 | onCanPlayThrough: ['audio', 'video'],
|
---|
| 82 | onClose: ['dialog'],
|
---|
| 83 | onDurationChange: ['audio', 'video'],
|
---|
| 84 | onEmptied: ['audio', 'video'],
|
---|
| 85 | onEncrypted: ['audio', 'video'],
|
---|
| 86 | onEnded: ['audio', 'video'],
|
---|
| 87 | onError: ['audio', 'video', 'img', 'link', 'source', 'script', 'picture', 'iframe'],
|
---|
| 88 | onLoad: ['script', 'img', 'link', 'picture', 'iframe', 'object', 'source'],
|
---|
| 89 | onLoadedData: ['audio', 'video'],
|
---|
| 90 | onLoadedMetadata: ['audio', 'video'],
|
---|
| 91 | onLoadStart: ['audio', 'video'],
|
---|
| 92 | onPause: ['audio', 'video'],
|
---|
| 93 | onPlay: ['audio', 'video'],
|
---|
| 94 | onPlaying: ['audio', 'video'],
|
---|
| 95 | onProgress: ['audio', 'video'],
|
---|
| 96 | onRateChange: ['audio', 'video'],
|
---|
| 97 | onResize: ['audio', 'video'],
|
---|
| 98 | onSeeked: ['audio', 'video'],
|
---|
| 99 | onSeeking: ['audio', 'video'],
|
---|
| 100 | onStalled: ['audio', 'video'],
|
---|
| 101 | onSuspend: ['audio', 'video'],
|
---|
| 102 | onTimeUpdate: ['audio', 'video'],
|
---|
| 103 | onVolumeChange: ['audio', 'video'],
|
---|
| 104 | onWaiting: ['audio', 'video'],
|
---|
| 105 | autoPictureInPicture: ['video'],
|
---|
| 106 | controls: ['audio', 'video'],
|
---|
| 107 | controlsList: ['audio', 'video'],
|
---|
| 108 | disablePictureInPicture: ['video'],
|
---|
| 109 | disableRemotePlayback: ['audio', 'video'],
|
---|
| 110 | loop: ['audio', 'video'],
|
---|
| 111 | muted: ['audio', 'video'],
|
---|
| 112 | playsInline: ['video'],
|
---|
| 113 | allowFullScreen: ['iframe', 'video'],
|
---|
| 114 | webkitAllowFullScreen: ['iframe', 'video'],
|
---|
| 115 | mozAllowFullScreen: ['iframe', 'video'],
|
---|
| 116 | poster: ['video'],
|
---|
| 117 | preload: ['audio', 'video'],
|
---|
| 118 | scrolling: ['iframe'],
|
---|
| 119 | returnValue: ['dialog'],
|
---|
| 120 | webkitDirectory: ['input'],
|
---|
| 121 | };
|
---|
| 122 |
|
---|
| 123 | const SVGDOM_ATTRIBUTE_NAMES = {
|
---|
| 124 | 'accent-height': 'accentHeight',
|
---|
| 125 | 'alignment-baseline': 'alignmentBaseline',
|
---|
| 126 | 'arabic-form': 'arabicForm',
|
---|
| 127 | 'baseline-shift': 'baselineShift',
|
---|
| 128 | 'cap-height': 'capHeight',
|
---|
| 129 | 'clip-path': 'clipPath',
|
---|
| 130 | 'clip-rule': 'clipRule',
|
---|
| 131 | 'color-interpolation': 'colorInterpolation',
|
---|
| 132 | 'color-interpolation-filters': 'colorInterpolationFilters',
|
---|
| 133 | 'color-profile': 'colorProfile',
|
---|
| 134 | 'color-rendering': 'colorRendering',
|
---|
| 135 | 'dominant-baseline': 'dominantBaseline',
|
---|
| 136 | 'enable-background': 'enableBackground',
|
---|
| 137 | 'fill-opacity': 'fillOpacity',
|
---|
| 138 | 'fill-rule': 'fillRule',
|
---|
| 139 | 'flood-color': 'floodColor',
|
---|
| 140 | 'flood-opacity': 'floodOpacity',
|
---|
| 141 | 'font-family': 'fontFamily',
|
---|
| 142 | 'font-size': 'fontSize',
|
---|
| 143 | 'font-size-adjust': 'fontSizeAdjust',
|
---|
| 144 | 'font-stretch': 'fontStretch',
|
---|
| 145 | 'font-style': 'fontStyle',
|
---|
| 146 | 'font-variant': 'fontVariant',
|
---|
| 147 | 'font-weight': 'fontWeight',
|
---|
| 148 | 'glyph-name': 'glyphName',
|
---|
| 149 | 'glyph-orientation-horizontal': 'glyphOrientationHorizontal',
|
---|
| 150 | 'glyph-orientation-vertical': 'glyphOrientationVertical',
|
---|
| 151 | 'horiz-adv-x': 'horizAdvX',
|
---|
| 152 | 'horiz-origin-x': 'horizOriginX',
|
---|
| 153 | 'image-rendering': 'imageRendering',
|
---|
| 154 | 'letter-spacing': 'letterSpacing',
|
---|
| 155 | 'lighting-color': 'lightingColor',
|
---|
| 156 | 'marker-end': 'markerEnd',
|
---|
| 157 | 'marker-mid': 'markerMid',
|
---|
| 158 | 'marker-start': 'markerStart',
|
---|
| 159 | 'overline-position': 'overlinePosition',
|
---|
| 160 | 'overline-thickness': 'overlineThickness',
|
---|
| 161 | 'paint-order': 'paintOrder',
|
---|
| 162 | 'panose-1': 'panose1',
|
---|
| 163 | 'pointer-events': 'pointerEvents',
|
---|
| 164 | 'rendering-intent': 'renderingIntent',
|
---|
| 165 | 'shape-rendering': 'shapeRendering',
|
---|
| 166 | 'stop-color': 'stopColor',
|
---|
| 167 | 'stop-opacity': 'stopOpacity',
|
---|
| 168 | 'strikethrough-position': 'strikethroughPosition',
|
---|
| 169 | 'strikethrough-thickness': 'strikethroughThickness',
|
---|
| 170 | 'stroke-dasharray': 'strokeDasharray',
|
---|
| 171 | 'stroke-dashoffset': 'strokeDashoffset',
|
---|
| 172 | 'stroke-linecap': 'strokeLinecap',
|
---|
| 173 | 'stroke-linejoin': 'strokeLinejoin',
|
---|
| 174 | 'stroke-miterlimit': 'strokeMiterlimit',
|
---|
| 175 | 'stroke-opacity': 'strokeOpacity',
|
---|
| 176 | 'stroke-width': 'strokeWidth',
|
---|
| 177 | 'text-anchor': 'textAnchor',
|
---|
| 178 | 'text-decoration': 'textDecoration',
|
---|
| 179 | 'text-rendering': 'textRendering',
|
---|
| 180 | 'underline-position': 'underlinePosition',
|
---|
| 181 | 'underline-thickness': 'underlineThickness',
|
---|
| 182 | 'unicode-bidi': 'unicodeBidi',
|
---|
| 183 | 'unicode-range': 'unicodeRange',
|
---|
| 184 | 'units-per-em': 'unitsPerEm',
|
---|
| 185 | 'v-alphabetic': 'vAlphabetic',
|
---|
| 186 | 'v-hanging': 'vHanging',
|
---|
| 187 | 'v-ideographic': 'vIdeographic',
|
---|
| 188 | 'v-mathematical': 'vMathematical',
|
---|
| 189 | 'vector-effect': 'vectorEffect',
|
---|
| 190 | 'vert-adv-y': 'vertAdvY',
|
---|
| 191 | 'vert-origin-x': 'vertOriginX',
|
---|
| 192 | 'vert-origin-y': 'vertOriginY',
|
---|
| 193 | 'word-spacing': 'wordSpacing',
|
---|
| 194 | 'writing-mode': 'writingMode',
|
---|
| 195 | 'x-height': 'xHeight',
|
---|
| 196 | 'xlink:actuate': 'xlinkActuate',
|
---|
| 197 | 'xlink:arcrole': 'xlinkArcrole',
|
---|
| 198 | 'xlink:href': 'xlinkHref',
|
---|
| 199 | 'xlink:role': 'xlinkRole',
|
---|
| 200 | 'xlink:show': 'xlinkShow',
|
---|
| 201 | 'xlink:title': 'xlinkTitle',
|
---|
| 202 | 'xlink:type': 'xlinkType',
|
---|
| 203 | 'xml:base': 'xmlBase',
|
---|
| 204 | 'xml:lang': 'xmlLang',
|
---|
| 205 | 'xml:space': 'xmlSpace',
|
---|
| 206 | };
|
---|
| 207 |
|
---|
| 208 | const DOM_PROPERTY_NAMES_ONE_WORD = [
|
---|
| 209 | // Global attributes - can be used on any HTML/DOM element
|
---|
| 210 | // See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
|
---|
| 211 | 'dir', 'draggable', 'hidden', 'id', 'lang', 'nonce', 'part', 'slot', 'style', 'title', 'translate', 'inert',
|
---|
| 212 | // Element specific attributes
|
---|
| 213 | // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
|
---|
| 214 | // To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
|
---|
| 215 | 'accept', 'action', 'allow', 'alt', 'as', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
|
---|
| 216 | 'content', 'coords', 'csp', 'data', 'decoding', 'default', 'defer', 'disabled', 'form',
|
---|
| 217 | 'headers', 'height', 'high', 'href', 'icon', 'importance', 'integrity', 'kind', 'label',
|
---|
| 218 | 'language', 'loading', 'list', 'loop', 'low', 'manifest', 'max', 'media', 'method', 'min', 'multiple', 'muted',
|
---|
| 219 | 'name', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'profile',
|
---|
| 220 | 'rel', 'required', 'reversed', 'role', 'rows', 'sandbox', 'scope', 'seamless', 'selected', 'shape', 'size', 'sizes',
|
---|
| 221 | 'span', 'src', 'start', 'step', 'summary', 'target', 'type', 'value', 'width', 'wmode', 'wrap',
|
---|
| 222 | // SVG attributes
|
---|
| 223 | // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
---|
| 224 | 'accumulate', 'additive', 'alphabetic', 'amplitude', 'ascent', 'azimuth', 'bbox', 'begin',
|
---|
| 225 | 'bias', 'by', 'clip', 'color', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'direction',
|
---|
| 226 | 'display', 'divisor', 'dur', 'dx', 'dy', 'elevation', 'end', 'exponent', 'fill', 'filter',
|
---|
| 227 | 'format', 'from', 'fr', 'fx', 'fy', 'g1', 'g2', 'hanging', 'height', 'hreflang', 'ideographic',
|
---|
| 228 | 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'local', 'mask', 'mode',
|
---|
| 229 | 'offset', 'opacity', 'operator', 'order', 'orient', 'orientation', 'origin', 'overflow', 'path',
|
---|
| 230 | 'ping', 'points', 'r', 'radius', 'rel', 'restart', 'result', 'rotate', 'rx', 'ry', 'scale',
|
---|
| 231 | 'seed', 'slope', 'spacing', 'speed', 'stemh', 'stemv', 'string', 'stroke', 'to', 'transform',
|
---|
| 232 | 'u1', 'u2', 'unicode', 'values', 'version', 'visibility', 'widths', 'x', 'x1', 'x2', 'xmlns',
|
---|
| 233 | 'y', 'y1', 'y2', 'z',
|
---|
| 234 | // OpenGraph meta tag attributes
|
---|
| 235 | 'property',
|
---|
| 236 | // React specific attributes
|
---|
| 237 | 'ref', 'key', 'children',
|
---|
| 238 | // Non-standard
|
---|
| 239 | 'results', 'security',
|
---|
| 240 | // Video specific
|
---|
| 241 | 'controls',
|
---|
| 242 | // popovers
|
---|
| 243 | 'popover', 'popovertarget', 'popovertargetaction',
|
---|
| 244 | ];
|
---|
| 245 |
|
---|
| 246 | const DOM_PROPERTY_NAMES_TWO_WORDS = [
|
---|
| 247 | // Global attributes - can be used on any HTML/DOM element
|
---|
| 248 | // See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
|
---|
| 249 | 'accessKey', 'autoCapitalize', 'autoFocus', 'contentEditable', 'enterKeyHint', 'exportParts',
|
---|
| 250 | 'inputMode', 'itemID', 'itemRef', 'itemProp', 'itemScope', 'itemType', 'spellCheck', 'tabIndex',
|
---|
| 251 | // Element specific attributes
|
---|
| 252 | // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
|
---|
| 253 | // To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
|
---|
| 254 | 'acceptCharset', 'autoComplete', 'autoPlay', 'border', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
|
---|
| 255 | 'colSpan', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
|
---|
| 256 | 'frameBorder', 'hrefLang', 'httpEquiv', 'imageSizes', 'imageSrcSet', 'isMap', 'keyParams', 'keyType', 'marginHeight', 'marginWidth',
|
---|
| 257 | 'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart',
|
---|
| 258 | 'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onCopy', 'onCompositionEnd', 'onCompositionStart',
|
---|
| 259 | 'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave',
|
---|
| 260 | 'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver',
|
---|
| 261 | 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver',
|
---|
| 262 | 'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onToggle', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
|
---|
| 263 | 'rowSpan', 'srcDoc', 'srcLang', 'srcSet', 'useMap', 'fetchPriority',
|
---|
| 264 | // SVG attributes
|
---|
| 265 | // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
---|
| 266 | 'crossOrigin', 'accentHeight', 'alignmentBaseline', 'arabicForm', 'attributeName',
|
---|
| 267 | 'attributeType', 'baseFrequency', 'baselineShift', 'baseProfile', 'calcMode', 'capHeight',
|
---|
| 268 | 'clipPathUnits', 'clipPath', 'clipRule', 'colorInterpolation', 'colorInterpolationFilters',
|
---|
| 269 | 'colorProfile', 'colorRendering', 'contentScriptType', 'contentStyleType', 'diffuseConstant',
|
---|
| 270 | 'dominantBaseline', 'edgeMode', 'enableBackground', 'fillOpacity', 'fillRule', 'filterRes',
|
---|
| 271 | 'filterUnits', 'floodColor', 'floodOpacity', 'fontFamily', 'fontSize', 'fontSizeAdjust',
|
---|
| 272 | 'fontStretch', 'fontStyle', 'fontVariant', 'fontWeight', 'glyphName',
|
---|
| 273 | 'glyphOrientationHorizontal', 'glyphOrientationVertical', 'glyphRef', 'gradientTransform',
|
---|
| 274 | 'gradientUnits', 'horizAdvX', 'horizOriginX', 'imageRendering', 'kernelMatrix',
|
---|
| 275 | 'kernelUnitLength', 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'letterSpacing',
|
---|
| 276 | 'lightingColor', 'limitingConeAngle', 'markerEnd', 'markerMid', 'markerStart', 'markerHeight',
|
---|
| 277 | 'markerUnits', 'markerWidth', 'maskContentUnits', 'maskUnits', 'mathematical', 'numOctaves',
|
---|
| 278 | 'overlinePosition', 'overlineThickness', 'panose1', 'paintOrder', 'pathLength',
|
---|
| 279 | 'patternContentUnits', 'patternTransform', 'patternUnits', 'pointerEvents', 'pointsAtX',
|
---|
| 280 | 'pointsAtY', 'pointsAtZ', 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits',
|
---|
| 281 | 'referrerPolicy', 'refX', 'refY', 'rendering-intent', 'repeatCount', 'repeatDur',
|
---|
| 282 | 'requiredExtensions', 'requiredFeatures', 'shapeRendering', 'specularConstant',
|
---|
| 283 | 'specularExponent', 'spreadMethod', 'startOffset', 'stdDeviation', 'stitchTiles', 'stopColor',
|
---|
| 284 | 'stopOpacity', 'strikethroughPosition', 'strikethroughThickness', 'strokeDasharray',
|
---|
| 285 | 'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'strokeOpacity',
|
---|
| 286 | 'strokeWidth', 'surfaceScale', 'systemLanguage', 'tableValues', 'targetX', 'targetY',
|
---|
| 287 | 'textAnchor', 'textDecoration', 'textRendering', 'textLength', 'transformOrigin',
|
---|
| 288 | 'underlinePosition', 'underlineThickness', 'unicodeBidi', 'unicodeRange', 'unitsPerEm',
|
---|
| 289 | 'vAlphabetic', 'vHanging', 'vIdeographic', 'vMathematical', 'vectorEffect', 'vertAdvY',
|
---|
| 290 | 'vertOriginX', 'vertOriginY', 'viewBox', 'viewTarget', 'wordSpacing', 'writingMode', 'xHeight',
|
---|
| 291 | 'xChannelSelector', 'xlinkActuate', 'xlinkArcrole', 'xlinkHref', 'xlinkRole', 'xlinkShow',
|
---|
| 292 | 'xlinkTitle', 'xlinkType', 'xmlBase', 'xmlLang', 'xmlnsXlink', 'xmlSpace', 'yChannelSelector',
|
---|
| 293 | 'zoomAndPan',
|
---|
| 294 | // Safari/Apple specific, no listing available
|
---|
| 295 | 'autoCorrect', // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
|
---|
| 296 | 'autoSave', // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
|
---|
| 297 | // React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
|
---|
| 298 | 'className', 'dangerouslySetInnerHTML', 'defaultValue', 'defaultChecked', 'htmlFor',
|
---|
| 299 | // Events' capture events
|
---|
| 300 | 'onBeforeInput', 'onChange',
|
---|
| 301 | 'onInvalid', 'onReset', 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'suppressContentEditableWarning', 'suppressHydrationWarning',
|
---|
| 302 | 'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
|
---|
| 303 | 'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange', 'onResize',
|
---|
| 304 | 'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting',
|
---|
| 305 | 'onCopyCapture', 'onCutCapture', 'onPasteCapture', 'onCompositionEndCapture', 'onCompositionStartCapture', 'onCompositionUpdateCapture',
|
---|
| 306 | 'onFocusCapture', 'onBlurCapture', 'onChangeCapture', 'onBeforeInputCapture', 'onInputCapture', 'onResetCapture', 'onSubmitCapture',
|
---|
| 307 | 'onInvalidCapture', 'onLoadCapture', 'onErrorCapture', 'onKeyDownCapture', 'onKeyPressCapture', 'onKeyUpCapture',
|
---|
| 308 | 'onAbortCapture', 'onCanPlayCapture', 'onCanPlayThroughCapture', 'onDurationChangeCapture', 'onEmptiedCapture', 'onEncryptedCapture',
|
---|
| 309 | 'onEndedCapture', 'onLoadedDataCapture', 'onLoadedMetadataCapture', 'onLoadStartCapture', 'onPauseCapture', 'onPlayCapture',
|
---|
| 310 | 'onPlayingCapture', 'onProgressCapture', 'onRateChangeCapture', 'onSeekedCapture', 'onSeekingCapture', 'onStalledCapture', 'onSuspendCapture',
|
---|
| 311 | 'onTimeUpdateCapture', 'onVolumeChangeCapture', 'onWaitingCapture', 'onSelectCapture', 'onTouchCancelCapture', 'onTouchEndCapture',
|
---|
| 312 | 'onTouchMoveCapture', 'onTouchStartCapture', 'onScrollCapture', 'onWheelCapture', 'onAnimationEndCapture', 'onAnimationIteration',
|
---|
| 313 | 'onAnimationStartCapture', 'onTransitionEndCapture',
|
---|
| 314 | 'onAuxClick', 'onAuxClickCapture', 'onClickCapture', 'onContextMenuCapture', 'onDoubleClickCapture',
|
---|
| 315 | 'onDragCapture', 'onDragEndCapture', 'onDragEnterCapture', 'onDragExitCapture', 'onDragLeaveCapture',
|
---|
| 316 | 'onDragOverCapture', 'onDragStartCapture', 'onDropCapture', 'onMouseDown', 'onMouseDownCapture',
|
---|
| 317 | 'onMouseMoveCapture', 'onMouseOutCapture', 'onMouseOverCapture', 'onMouseUpCapture',
|
---|
| 318 | // Video specific
|
---|
| 319 | 'autoPictureInPicture', 'controlsList', 'disablePictureInPicture', 'disableRemotePlayback',
|
---|
| 320 | ];
|
---|
| 321 |
|
---|
| 322 | const DOM_PROPERTIES_IGNORE_CASE = ['charset', 'allowFullScreen', 'webkitAllowFullScreen', 'mozAllowFullScreen', 'webkitDirectory'];
|
---|
| 323 |
|
---|
| 324 | const ARIA_PROPERTIES = [
|
---|
| 325 | // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
---|
| 326 | // Global attributes
|
---|
| 327 | 'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current',
|
---|
| 328 | 'aria-describedby', 'aria-description', 'aria-details',
|
---|
| 329 | 'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup',
|
---|
| 330 | 'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live',
|
---|
| 331 | 'aria-owns', 'aria-relevant', 'aria-roledescription',
|
---|
| 332 | // Widget attributes
|
---|
| 333 | 'aria-autocomplete', 'aria-checked', 'aria-expanded', 'aria-level', 'aria-modal', 'aria-multiline', 'aria-multiselectable',
|
---|
| 334 | 'aria-orientation', 'aria-placeholder', 'aria-pressed', 'aria-readonly', 'aria-required', 'aria-selected',
|
---|
| 335 | 'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
|
---|
| 336 | // Relationship attributes
|
---|
| 337 | 'aria-activedescendant', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan',
|
---|
| 338 | 'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
|
---|
| 339 | ];
|
---|
| 340 |
|
---|
| 341 | const REACT_ON_PROPS = [
|
---|
| 342 | 'onGotPointerCapture',
|
---|
| 343 | 'onGotPointerCaptureCapture',
|
---|
| 344 | 'onLostPointerCapture',
|
---|
| 345 | 'onLostPointerCapture',
|
---|
| 346 | 'onLostPointerCaptureCapture',
|
---|
| 347 | 'onPointerCancel',
|
---|
| 348 | 'onPointerCancelCapture',
|
---|
| 349 | 'onPointerDown',
|
---|
| 350 | 'onPointerDownCapture',
|
---|
| 351 | 'onPointerEnter',
|
---|
| 352 | 'onPointerEnterCapture',
|
---|
| 353 | 'onPointerLeave',
|
---|
| 354 | 'onPointerLeaveCapture',
|
---|
| 355 | 'onPointerMove',
|
---|
| 356 | 'onPointerMoveCapture',
|
---|
| 357 | 'onPointerOut',
|
---|
| 358 | 'onPointerOutCapture',
|
---|
| 359 | 'onPointerOver',
|
---|
| 360 | 'onPointerOverCapture',
|
---|
| 361 | 'onPointerUp',
|
---|
| 362 | 'onPointerUpCapture',
|
---|
| 363 | ];
|
---|
| 364 |
|
---|
| 365 | function getDOMPropertyNames(context) {
|
---|
| 366 | const ALL_DOM_PROPERTY_NAMES = DOM_PROPERTY_NAMES_TWO_WORDS.concat(DOM_PROPERTY_NAMES_ONE_WORD);
|
---|
| 367 | // this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
|
---|
| 368 | if (!testReactVersion(context, '>= 16.1.0')) {
|
---|
| 369 | return ALL_DOM_PROPERTY_NAMES.concat('allowTransparency');
|
---|
| 370 | }
|
---|
| 371 | // these were added in React v16.4.0, see https://reactjs.org/blog/2018/05/23/react-v-16-4.html and https://github.com/facebook/react/pull/12507
|
---|
| 372 | if (testReactVersion(context, '>= 16.4.0')) {
|
---|
| 373 | return ALL_DOM_PROPERTY_NAMES.concat(REACT_ON_PROPS);
|
---|
| 374 | }
|
---|
| 375 | return ALL_DOM_PROPERTY_NAMES;
|
---|
| 376 | }
|
---|
| 377 |
|
---|
| 378 | // ------------------------------------------------------------------------------
|
---|
| 379 | // Helpers
|
---|
| 380 | // ------------------------------------------------------------------------------
|
---|
| 381 |
|
---|
| 382 | /**
|
---|
| 383 | * Checks if a node's parent is a JSX tag that is written with lowercase letters,
|
---|
| 384 | * and is not a custom web component. Custom web components have a hyphen in tag name,
|
---|
| 385 | * or have an `is="some-elem"` attribute.
|
---|
| 386 | *
|
---|
| 387 | * Note: does not check if a tag's parent against a list of standard HTML/DOM tags. For example,
|
---|
| 388 | * a `<fake>`'s child would return `true` because "fake" is written only with lowercase letters
|
---|
| 389 | * without a hyphen and does not have a `is="some-elem"` attribute.
|
---|
| 390 | *
|
---|
| 391 | * @param {Object} childNode - JSX element being tested.
|
---|
| 392 | * @returns {boolean} Whether or not the node name match the JSX tag convention.
|
---|
| 393 | */
|
---|
| 394 | function isValidHTMLTagInJSX(childNode) {
|
---|
| 395 | const tagConvention = /^[a-z][^-]*$/;
|
---|
| 396 | if (tagConvention.test(childNode.parent.name.name)) {
|
---|
| 397 | return !childNode.parent.attributes.some((attrNode) => (
|
---|
| 398 | attrNode.type === 'JSXAttribute'
|
---|
| 399 | && attrNode.name.type === 'JSXIdentifier'
|
---|
| 400 | && attrNode.name.name === 'is'
|
---|
| 401 | // To learn more about custom web components and `is` attribute,
|
---|
| 402 | // see https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-customized-builtin-example
|
---|
| 403 |
|
---|
| 404 | ));
|
---|
| 405 | }
|
---|
| 406 | return false;
|
---|
| 407 | }
|
---|
| 408 |
|
---|
| 409 | /**
|
---|
| 410 | * Checks if the attribute name is included in the attributes that are excluded
|
---|
| 411 | * from the camel casing.
|
---|
| 412 | *
|
---|
| 413 | * // returns 'charSet'
|
---|
| 414 | * @example normalizeAttributeCase('charset')
|
---|
| 415 | *
|
---|
| 416 | * Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
|
---|
| 417 | *
|
---|
| 418 | * @param {String} name - Attribute name to be normalized
|
---|
| 419 | * @returns {String} Result
|
---|
| 420 | */
|
---|
| 421 | function normalizeAttributeCase(name) {
|
---|
| 422 | return DOM_PROPERTIES_IGNORE_CASE.find((element) => element.toLowerCase() === name.toLowerCase()) || name;
|
---|
| 423 | }
|
---|
| 424 |
|
---|
| 425 | /**
|
---|
| 426 | * Checks if an attribute name is a valid `data-*` attribute:
|
---|
| 427 | * if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
|
---|
| 428 | * not start with any casing of "xml", and separated by hyphens (-) (which is also called "kebab case" or "dash case"),
|
---|
| 429 | * then the attribute is a valid data attribute.
|
---|
| 430 | *
|
---|
| 431 | * @param {String} name - Attribute name to be tested
|
---|
| 432 | * @returns {boolean} Result
|
---|
| 433 | */
|
---|
| 434 | function isValidDataAttribute(name) {
|
---|
| 435 | return !/^data-xml/i.test(name) && /^data-[^:]*$/.test(name);
|
---|
| 436 | }
|
---|
| 437 |
|
---|
| 438 | /**
|
---|
| 439 | * Checks if an attribute name has at least one uppercase characters
|
---|
| 440 | *
|
---|
| 441 | * @param {String} name
|
---|
| 442 | * @returns {boolean} Result
|
---|
| 443 | */
|
---|
| 444 | function hasUpperCaseCharacter(name) {
|
---|
| 445 | return name.toLowerCase() !== name;
|
---|
| 446 | }
|
---|
| 447 |
|
---|
| 448 | /**
|
---|
| 449 | * Checks if an attribute name is a standard aria attribute by compering it to a list
|
---|
| 450 | * of standard aria property names
|
---|
| 451 | *
|
---|
| 452 | * @param {String} name - Attribute name to be tested
|
---|
| 453 | * @returns {Boolean} Result
|
---|
| 454 | */
|
---|
| 455 |
|
---|
| 456 | function isValidAriaAttribute(name) {
|
---|
| 457 | return ARIA_PROPERTIES.some((element) => element === name);
|
---|
| 458 | }
|
---|
| 459 |
|
---|
| 460 | /**
|
---|
| 461 | * Extracts the tag name for the JSXAttribute
|
---|
| 462 | * @param {JSXAttribute} node - JSXAttribute being tested.
|
---|
| 463 | * @returns {String|null} tag name
|
---|
| 464 | */
|
---|
| 465 | function getTagName(node) {
|
---|
| 466 | if (node && node.parent && node.parent.name && node.parent.name) {
|
---|
| 467 | return node.parent.name.name;
|
---|
| 468 | }
|
---|
| 469 | return null;
|
---|
| 470 | }
|
---|
| 471 |
|
---|
| 472 | /**
|
---|
| 473 | * Test wether the tag name for the JSXAttribute is
|
---|
| 474 | * something like <Foo.bar />
|
---|
| 475 | * @param {JSXAttribute} node - JSXAttribute being tested.
|
---|
| 476 | * @returns {Boolean} result
|
---|
| 477 | */
|
---|
| 478 | function tagNameHasDot(node) {
|
---|
| 479 | return !!(
|
---|
| 480 | node.parent
|
---|
| 481 | && node.parent.name
|
---|
| 482 | && node.parent.name.type === 'JSXMemberExpression'
|
---|
| 483 | );
|
---|
| 484 | }
|
---|
| 485 |
|
---|
| 486 | /**
|
---|
| 487 | * Get the standard name of the attribute.
|
---|
| 488 | * @param {String} name - Name of the attribute.
|
---|
| 489 | * @param {String} context - eslint context
|
---|
| 490 | * @returns {String | undefined} The standard name of the attribute, or undefined if no standard name was found.
|
---|
| 491 | */
|
---|
| 492 | function getStandardName(name, context) {
|
---|
| 493 | if (has(DOM_ATTRIBUTE_NAMES, name)) {
|
---|
| 494 | return DOM_ATTRIBUTE_NAMES[/** @type {keyof DOM_ATTRIBUTE_NAMES} */ (name)];
|
---|
| 495 | }
|
---|
| 496 | if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
|
---|
| 497 | return SVGDOM_ATTRIBUTE_NAMES[/** @type {keyof SVGDOM_ATTRIBUTE_NAMES} */ (name)];
|
---|
| 498 | }
|
---|
| 499 | const names = getDOMPropertyNames(context);
|
---|
| 500 | // Let's find a possible attribute match with a case-insensitive search.
|
---|
| 501 | return names.find((element) => element.toLowerCase() === name.toLowerCase());
|
---|
| 502 | }
|
---|
| 503 |
|
---|
| 504 | // ------------------------------------------------------------------------------
|
---|
| 505 | // Rule Definition
|
---|
| 506 | // ------------------------------------------------------------------------------
|
---|
| 507 |
|
---|
| 508 | const messages = {
|
---|
| 509 | invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
|
---|
| 510 | unknownPropWithStandardName: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead',
|
---|
| 511 | unknownProp: 'Unknown property \'{{name}}\' found',
|
---|
| 512 | dataLowercaseRequired: 'React does not recognize data-* props with uppercase characters on a DOM element. Found \'{{name}}\', use \'{{lowerCaseName}}\' instead',
|
---|
| 513 | };
|
---|
| 514 |
|
---|
| 515 | module.exports = {
|
---|
| 516 | meta: {
|
---|
| 517 | docs: {
|
---|
| 518 | description: 'Disallow usage of unknown DOM property',
|
---|
| 519 | category: 'Possible Errors',
|
---|
| 520 | recommended: true,
|
---|
| 521 | url: docsUrl('no-unknown-property'),
|
---|
| 522 | },
|
---|
| 523 | fixable: 'code',
|
---|
| 524 |
|
---|
| 525 | messages,
|
---|
| 526 |
|
---|
| 527 | schema: [{
|
---|
| 528 | type: 'object',
|
---|
| 529 | properties: {
|
---|
| 530 | ignore: {
|
---|
| 531 | type: 'array',
|
---|
| 532 | items: {
|
---|
| 533 | type: 'string',
|
---|
| 534 | },
|
---|
| 535 | },
|
---|
| 536 | requireDataLowercase: {
|
---|
| 537 | type: 'boolean',
|
---|
| 538 | default: false,
|
---|
| 539 | },
|
---|
| 540 | },
|
---|
| 541 | additionalProperties: false,
|
---|
| 542 | }],
|
---|
| 543 | },
|
---|
| 544 |
|
---|
| 545 | create(context) {
|
---|
| 546 | function getIgnoreConfig() {
|
---|
| 547 | return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
|
---|
| 548 | }
|
---|
| 549 |
|
---|
| 550 | function getRequireDataLowercase() {
|
---|
| 551 | return (context.options[0] && typeof context.options[0].requireDataLowercase !== 'undefined')
|
---|
| 552 | ? !!context.options[0].requireDataLowercase
|
---|
| 553 | : DEFAULTS.requireDataLowercase;
|
---|
| 554 | }
|
---|
| 555 |
|
---|
| 556 | return {
|
---|
| 557 | JSXAttribute(node) {
|
---|
| 558 | const ignoreNames = getIgnoreConfig();
|
---|
| 559 | const actualName = getText(context, node.name);
|
---|
| 560 | if (ignoreNames.indexOf(actualName) >= 0) {
|
---|
| 561 | return;
|
---|
| 562 | }
|
---|
| 563 | const name = normalizeAttributeCase(actualName);
|
---|
| 564 |
|
---|
| 565 | // Ignore tags like <Foo.bar />
|
---|
| 566 | if (tagNameHasDot(node)) {
|
---|
| 567 | return;
|
---|
| 568 | }
|
---|
| 569 |
|
---|
| 570 | if (isValidDataAttribute(name)) {
|
---|
| 571 | if (getRequireDataLowercase() && hasUpperCaseCharacter(name)) {
|
---|
| 572 | report(context, messages.dataLowercaseRequired, 'dataLowercaseRequired', {
|
---|
| 573 | node,
|
---|
| 574 | data: {
|
---|
| 575 | name: actualName,
|
---|
| 576 | lowerCaseName: actualName.toLowerCase(),
|
---|
| 577 | },
|
---|
| 578 | });
|
---|
| 579 | }
|
---|
| 580 |
|
---|
| 581 | return;
|
---|
| 582 | }
|
---|
| 583 |
|
---|
| 584 | if (isValidAriaAttribute(name)) { return; }
|
---|
| 585 |
|
---|
| 586 | const tagName = getTagName(node);
|
---|
| 587 |
|
---|
| 588 | if (tagName === 'fbt' || tagName === 'fbs') { return; } // fbt/fbs nodes are bonkers, let's not go there
|
---|
| 589 |
|
---|
| 590 | if (!isValidHTMLTagInJSX(node)) { return; }
|
---|
| 591 |
|
---|
| 592 | // Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
|
---|
| 593 |
|
---|
| 594 | // Some attributes are allowed on some tags only
|
---|
| 595 | const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[/** @type {keyof ATTRIBUTE_TAGS_MAP} */ (name)] : null;
|
---|
| 596 | if (tagName && allowedTags) {
|
---|
| 597 | // Scenario 1A: Allowed attribute found where not supposed to, report it
|
---|
| 598 | if (allowedTags.indexOf(tagName) === -1) {
|
---|
| 599 | report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
|
---|
| 600 | node,
|
---|
| 601 | data: {
|
---|
| 602 | name: actualName,
|
---|
| 603 | tagName,
|
---|
| 604 | allowedTags: allowedTags.join(', '),
|
---|
| 605 | },
|
---|
| 606 | });
|
---|
| 607 | }
|
---|
| 608 | // Scenario 1B: There are allowed attributes on allowed tags, no need to report it
|
---|
| 609 | return;
|
---|
| 610 | }
|
---|
| 611 |
|
---|
| 612 | // Let's see if the attribute is a close version to some standard property name
|
---|
| 613 | const standardName = getStandardName(name, context);
|
---|
| 614 |
|
---|
| 615 | const hasStandardNameButIsNotUsed = standardName && standardName !== name;
|
---|
| 616 | const usesStandardName = standardName && standardName === name;
|
---|
| 617 |
|
---|
| 618 | if (usesStandardName) {
|
---|
| 619 | // Scenario 2A: The attribute name is the standard name, no need to report it
|
---|
| 620 | return;
|
---|
| 621 | }
|
---|
| 622 |
|
---|
| 623 | if (hasStandardNameButIsNotUsed) {
|
---|
| 624 | // Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
|
---|
| 625 | report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
|
---|
| 626 | node,
|
---|
| 627 | data: {
|
---|
| 628 | name: actualName,
|
---|
| 629 | standardName,
|
---|
| 630 | },
|
---|
| 631 | fix(fixer) {
|
---|
| 632 | return fixer.replaceText(node.name, standardName);
|
---|
| 633 | },
|
---|
| 634 | });
|
---|
| 635 | return;
|
---|
| 636 | }
|
---|
| 637 |
|
---|
| 638 | // Scenario 3: We have an attribute that is unknown, report it
|
---|
| 639 | report(context, messages.unknownProp, 'unknownProp', {
|
---|
| 640 | node,
|
---|
| 641 | data: {
|
---|
| 642 | name: actualName,
|
---|
| 643 | },
|
---|
| 644 | });
|
---|
| 645 | },
|
---|
| 646 | };
|
---|
| 647 | },
|
---|
| 648 | };
|
---|