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

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

Update repo after prototype presentation

  • Property mode set to 100644
File size: 27.0 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 (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 */
478function 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 */
492function 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
508const 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
515module.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};
Note: See TracBrowser for help on using the repository browser.