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 | };
|
---|