source: frontend/node_modules/eslint-plugin-react/lib/rules/no-unknown-property.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 13 days ago

Fix frontend appearance

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