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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

  • Property mode set to 100644
File size: 27.1 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};
122
123const 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
208const 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
246const 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
322const DOM_PROPERTIES_IGNORE_CASE = ['charset', 'allowFullScreen', 'webkitAllowFullScreen', 'mozAllowFullScreen', 'webkitDirectory'];
323
324const 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
341const 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
365function 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 */
394function 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 */
421function 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 */
434function 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 */
444function 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
456function 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 */
465function getTagName(node) {
466 if (
467 node
468 && node.parent
469 && node.parent.name
470 ) {
471 return node.parent.name.name;
472 }
473 return null;
474}
475
476/**
477 * Test wether the tag name for the JSXAttribute is
478 * something like <Foo.bar />
479 * @param {JSXAttribute} node - JSXAttribute being tested.
480 * @returns {boolean} result
481 */
482function tagNameHasDot(node) {
483 return !!(
484 node.parent
485 && node.parent.name
486 && node.parent.name.type === 'JSXMemberExpression'
487 );
488}
489
490/**
491 * Get the standard name of the attribute.
492 * @param {string} name - Name of the attribute.
493 * @param {object} context - eslint context
494 * @returns {string | undefined} The standard name of the attribute, or undefined if no standard name was found.
495 */
496function getStandardName(name, context) {
497 if (has(DOM_ATTRIBUTE_NAMES, name)) {
498 return DOM_ATTRIBUTE_NAMES[/** @type {keyof DOM_ATTRIBUTE_NAMES} */ (name)];
499 }
500 if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
501 return SVGDOM_ATTRIBUTE_NAMES[/** @type {keyof SVGDOM_ATTRIBUTE_NAMES} */ (name)];
502 }
503 const names = getDOMPropertyNames(context);
504 // Let's find a possible attribute match with a case-insensitive search.
505 return names.find((element) => element.toLowerCase() === name.toLowerCase());
506}
507
508// ------------------------------------------------------------------------------
509// Rule Definition
510// ------------------------------------------------------------------------------
511
512const messages = {
513 invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
514 unknownPropWithStandardName: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead',
515 unknownProp: 'Unknown property \'{{name}}\' found',
516 dataLowercaseRequired: 'React does not recognize data-* props with uppercase characters on a DOM element. Found \'{{name}}\', use \'{{lowerCaseName}}\' instead',
517};
518
519/** @type {import('eslint').Rule.RuleModule} */
520module.exports = {
521 meta: {
522 docs: {
523 description: 'Disallow usage of unknown DOM property',
524 category: 'Possible Errors',
525 recommended: true,
526 url: docsUrl('no-unknown-property'),
527 },
528 fixable: 'code',
529
530 messages,
531
532 schema: [{
533 type: 'object',
534 properties: {
535 ignore: {
536 type: 'array',
537 items: {
538 type: 'string',
539 },
540 },
541 requireDataLowercase: {
542 type: 'boolean',
543 default: false,
544 },
545 },
546 additionalProperties: false,
547 }],
548 },
549
550 create(context) {
551 function getIgnoreConfig() {
552 return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
553 }
554
555 function getRequireDataLowercase() {
556 return (context.options[0] && typeof context.options[0].requireDataLowercase !== 'undefined')
557 ? !!context.options[0].requireDataLowercase
558 : DEFAULTS.requireDataLowercase;
559 }
560
561 return {
562 JSXAttribute(node) {
563 const ignoreNames = getIgnoreConfig();
564 const actualName = getText(context, node.name);
565 if (ignoreNames.indexOf(actualName) >= 0) {
566 return;
567 }
568 const name = normalizeAttributeCase(actualName);
569
570 // Ignore tags like <Foo.bar />
571 if (tagNameHasDot(node)) {
572 return;
573 }
574
575 if (isValidDataAttribute(name)) {
576 if (getRequireDataLowercase() && hasUpperCaseCharacter(name)) {
577 report(context, messages.dataLowercaseRequired, 'dataLowercaseRequired', {
578 node,
579 data: {
580 name: actualName,
581 lowerCaseName: actualName.toLowerCase(),
582 },
583 });
584 }
585
586 return;
587 }
588
589 if (isValidAriaAttribute(name)) { return; }
590
591 const tagName = getTagName(node);
592
593 if (tagName === 'fbt' || tagName === 'fbs') { return; } // fbt/fbs nodes are bonkers, let's not go there
594
595 if (!isValidHTMLTagInJSX(node)) { return; }
596
597 // Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
598
599 // Some attributes are allowed on some tags only
600 const allowedTags = has(ATTRIBUTE_TAGS_MAP, name)
601 ? ATTRIBUTE_TAGS_MAP[/** @type {keyof ATTRIBUTE_TAGS_MAP} */ (name)]
602 : null;
603 if (tagName && allowedTags) {
604 // Scenario 1A: Allowed attribute found where not supposed to, report it
605 if (allowedTags.indexOf(tagName) === -1) {
606 report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
607 node,
608 data: {
609 name: actualName,
610 tagName,
611 allowedTags: allowedTags.join(', '),
612 },
613 });
614 }
615 // Scenario 1B: There are allowed attributes on allowed tags, no need to report it
616 return;
617 }
618
619 // Let's see if the attribute is a close version to some standard property name
620 const standardName = getStandardName(name, context);
621
622 const hasStandardNameButIsNotUsed = standardName && standardName !== name;
623 const usesStandardName = standardName && standardName === name;
624
625 if (usesStandardName) {
626 // Scenario 2A: The attribute name is the standard name, no need to report it
627 return;
628 }
629
630 if (hasStandardNameButIsNotUsed) {
631 // Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
632 report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
633 node,
634 data: {
635 name: actualName,
636 standardName,
637 },
638 fix(fixer) {
639 return fixer.replaceText(node.name, standardName);
640 },
641 });
642 return;
643 }
644
645 // Scenario 3: We have an attribute that is unknown, report it
646 report(context, messages.unknownProp, 'unknownProp', {
647 node,
648 data: {
649 name: actualName,
650 },
651 });
652 },
653 };
654 },
655};
Note: See TracBrowser for help on using the repository browser.