| /** |
| * @fileoverview Prevent usage of unknown DOM property |
| * @author Yannick Croissant |
| */ |
| |
| 'use strict'; |
| |
| const docsUrl = require('../util/docsUrl'); |
| |
| // ------------------------------------------------------------------------------ |
| // Constants |
| // ------------------------------------------------------------------------------ |
| |
| const DEFAULTS = { |
| ignore: [] |
| }; |
| |
| const UNKNOWN_MESSAGE = 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead'; |
| const WRONG_TAG_MESSAGE = 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}'; |
| |
| const DOM_ATTRIBUTE_NAMES = { |
| 'accept-charset': 'acceptCharset', |
| class: 'className', |
| for: 'htmlFor', |
| 'http-equiv': 'httpEquiv', |
| crossorigin: 'crossOrigin' |
| }; |
| |
| const ATTRIBUTE_TAGS_MAP = { |
| crossOrigin: ['script', 'img', 'video', 'audio', 'link'] |
| }; |
| |
| const SVGDOM_ATTRIBUTE_NAMES = { |
| 'accent-height': 'accentHeight', |
| 'alignment-baseline': 'alignmentBaseline', |
| 'arabic-form': 'arabicForm', |
| 'baseline-shift': 'baselineShift', |
| 'cap-height': 'capHeight', |
| 'clip-path': 'clipPath', |
| 'clip-rule': 'clipRule', |
| 'color-interpolation': 'colorInterpolation', |
| 'color-interpolation-filters': 'colorInterpolationFilters', |
| 'color-profile': 'colorProfile', |
| 'color-rendering': 'colorRendering', |
| 'dominant-baseline': 'dominantBaseline', |
| 'enable-background': 'enableBackground', |
| 'fill-opacity': 'fillOpacity', |
| 'fill-rule': 'fillRule', |
| 'flood-color': 'floodColor', |
| 'flood-opacity': 'floodOpacity', |
| 'font-family': 'fontFamily', |
| 'font-size': 'fontSize', |
| 'font-size-adjust': 'fontSizeAdjust', |
| 'font-stretch': 'fontStretch', |
| 'font-style': 'fontStyle', |
| 'font-variant': 'fontVariant', |
| 'font-weight': 'fontWeight', |
| 'glyph-name': 'glyphName', |
| 'glyph-orientation-horizontal': 'glyphOrientationHorizontal', |
| 'glyph-orientation-vertical': 'glyphOrientationVertical', |
| 'horiz-adv-x': 'horizAdvX', |
| 'horiz-origin-x': 'horizOriginX', |
| 'image-rendering': 'imageRendering', |
| 'letter-spacing': 'letterSpacing', |
| 'lighting-color': 'lightingColor', |
| 'marker-end': 'markerEnd', |
| 'marker-mid': 'markerMid', |
| 'marker-start': 'markerStart', |
| 'overline-position': 'overlinePosition', |
| 'overline-thickness': 'overlineThickness', |
| 'paint-order': 'paintOrder', |
| 'panose-1': 'panose1', |
| 'pointer-events': 'pointerEvents', |
| 'rendering-intent': 'renderingIntent', |
| 'shape-rendering': 'shapeRendering', |
| 'stop-color': 'stopColor', |
| 'stop-opacity': 'stopOpacity', |
| 'strikethrough-position': 'strikethroughPosition', |
| 'strikethrough-thickness': 'strikethroughThickness', |
| 'stroke-dasharray': 'strokeDasharray', |
| 'stroke-dashoffset': 'strokeDashoffset', |
| 'stroke-linecap': 'strokeLinecap', |
| 'stroke-linejoin': 'strokeLinejoin', |
| 'stroke-miterlimit': 'strokeMiterlimit', |
| 'stroke-opacity': 'strokeOpacity', |
| 'stroke-width': 'strokeWidth', |
| 'text-anchor': 'textAnchor', |
| 'text-decoration': 'textDecoration', |
| 'text-rendering': 'textRendering', |
| 'underline-position': 'underlinePosition', |
| 'underline-thickness': 'underlineThickness', |
| 'unicode-bidi': 'unicodeBidi', |
| 'unicode-range': 'unicodeRange', |
| 'units-per-em': 'unitsPerEm', |
| 'v-alphabetic': 'vAlphabetic', |
| 'v-hanging': 'vHanging', |
| 'v-ideographic': 'vIdeographic', |
| 'v-mathematical': 'vMathematical', |
| 'vector-effect': 'vectorEffect', |
| 'vert-adv-y': 'vertAdvY', |
| 'vert-origin-x': 'vertOriginX', |
| 'vert-origin-y': 'vertOriginY', |
| 'word-spacing': 'wordSpacing', |
| 'writing-mode': 'writingMode', |
| 'x-height': 'xHeight', |
| 'xlink:actuate': 'xlinkActuate', |
| 'xlink:arcrole': 'xlinkArcrole', |
| 'xlink:href': 'xlinkHref', |
| 'xlink:role': 'xlinkRole', |
| 'xlink:show': 'xlinkShow', |
| 'xlink:title': 'xlinkTitle', |
| 'xlink:type': 'xlinkType', |
| 'xml:base': 'xmlBase', |
| 'xml:lang': 'xmlLang', |
| 'xml:space': 'xmlSpace' |
| }; |
| |
| const DOM_PROPERTY_NAMES = [ |
| // Standard |
| 'acceptCharset', 'accessKey', 'allowFullScreen', 'allowTransparency', 'autoComplete', 'autoFocus', 'autoPlay', |
| 'cellPadding', 'cellSpacing', 'classID', 'className', 'colSpan', 'contentEditable', 'contextMenu', |
| 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget', |
| 'frameBorder', 'hrefLang', 'htmlFor', 'httpEquiv', 'inputMode', 'keyParams', 'keyType', 'marginHeight', 'marginWidth', |
| 'maxLength', 'mediaGroup', 'minLength', 'noValidate', 'onAnimationEnd', 'onAnimationIteration', 'onAnimationStart', |
| 'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onCopy', 'onCompositionEnd', 'onCompositionStart', |
| 'onCompositionUpdate', 'onCut', 'onDoubleClick', 'onDrag', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', |
| 'onError', 'onFocus', 'onInput', 'onKeyDown', 'onKeyPress', 'onKeyUp', 'onLoad', 'onWheel', 'onDragOver', |
| 'onDragStart', 'onDrop', 'onMouseDown', 'onMouseEnter', 'onMouseLeave', 'onMouseMove', 'onMouseOut', 'onMouseOver', |
| 'onMouseUp', 'onPaste', 'onScroll', 'onSelect', 'onSubmit', 'onTransitionEnd', 'radioGroup', 'readOnly', 'rowSpan', |
| 'spellCheck', 'srcDoc', 'srcLang', 'srcSet', 'tabIndex', 'useMap', |
| // Non standard |
| 'autoCapitalize', 'autoCorrect', |
| 'autoSave', |
| 'itemProp', 'itemScope', 'itemType', 'itemRef', 'itemID' |
| ]; |
| |
| // ------------------------------------------------------------------------------ |
| // Helpers |
| // ------------------------------------------------------------------------------ |
| |
| /** |
| * Checks if a node matches the JSX tag convention. |
| * @param {Object} node - JSX element being tested. |
| * @returns {boolean} Whether or not the node name match the JSX tag convention. |
| */ |
| const tagConvention = /^[a-z][^-]*$/; |
| function isTagName(node) { |
| if (tagConvention.test(node.parent.name.name)) { |
| // http://www.w3.org/TR/custom-elements/#type-extension-semantics |
| return !node.parent.attributes.some(attrNode => ( |
| attrNode.type === 'JSXAttribute' && |
| attrNode.name.type === 'JSXIdentifier' && |
| attrNode.name.name === 'is' |
| )); |
| } |
| return false; |
| } |
| |
| /** |
| * Extracts the tag name for the JSXAttribute |
| * @param {JSXAttribute} node - JSXAttribute being tested. |
| * @returns {String|null} tag name |
| */ |
| function getTagName(node) { |
| if (node && node.parent && node.parent.name && node.parent.name) { |
| return node.parent.name.name; |
| } |
| return null; |
| } |
| |
| /** |
| * Test wether the tag name for the JSXAttribute is |
| * something like <Foo.bar /> |
| * @param {JSXAttribute} node - JSXAttribute being tested. |
| * @returns {Boolean} result |
| */ |
| function tagNameHasDot(node) { |
| return !!( |
| node.parent && |
| node.parent.name && |
| node.parent.name.type === 'JSXMemberExpression' |
| ); |
| } |
| |
| /** |
| * Get the standard name of the attribute. |
| * @param {String} name - Name of the attribute. |
| * @returns {String} The standard name of the attribute. |
| */ |
| function getStandardName(name) { |
| if (DOM_ATTRIBUTE_NAMES[name]) { |
| return DOM_ATTRIBUTE_NAMES[name]; |
| } |
| if (SVGDOM_ATTRIBUTE_NAMES[name]) { |
| return SVGDOM_ATTRIBUTE_NAMES[name]; |
| } |
| let i = -1; |
| const found = DOM_PROPERTY_NAMES.some((element, index) => { |
| i = index; |
| return element.toLowerCase() === name; |
| }); |
| return found ? DOM_PROPERTY_NAMES[i] : null; |
| } |
| |
| // ------------------------------------------------------------------------------ |
| // Rule Definition |
| // ------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| docs: { |
| description: 'Prevent usage of unknown DOM property', |
| category: 'Possible Errors', |
| recommended: true, |
| url: docsUrl('no-unknown-property') |
| }, |
| fixable: 'code', |
| |
| schema: [{ |
| type: 'object', |
| properties: { |
| ignore: { |
| type: 'array', |
| items: { |
| type: 'string' |
| } |
| } |
| }, |
| additionalProperties: false |
| }] |
| }, |
| |
| create(context) { |
| function getIgnoreConfig() { |
| return context.options[0] && context.options[0].ignore || DEFAULTS.ignore; |
| } |
| |
| return { |
| JSXAttribute(node) { |
| const ignoreNames = getIgnoreConfig(); |
| const name = context.getSourceCode().getText(node.name); |
| if (ignoreNames.indexOf(name) >= 0) { |
| return; |
| } |
| |
| // Ignore tags like <Foo.bar /> |
| if (tagNameHasDot(node)) { |
| return; |
| } |
| |
| const tagName = getTagName(node); |
| const allowedTags = ATTRIBUTE_TAGS_MAP[name]; |
| if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) { |
| context.report({ |
| node, |
| message: WRONG_TAG_MESSAGE, |
| data: { |
| name, |
| tagName, |
| allowedTags: allowedTags.join(', ') |
| } |
| }); |
| } |
| |
| const standardName = getStandardName(name); |
| if (!isTagName(node) || !standardName) { |
| return; |
| } |
| context.report({ |
| node, |
| message: UNKNOWN_MESSAGE, |
| data: { |
| name, |
| standardName |
| }, |
| fix(fixer) { |
| return fixer.replaceText(node.name, standardName); |
| } |
| }); |
| } |
| }; |
| } |
| }; |