GoScrobble/web/node_modules/eslint-plugin-react/lib/rules/no-unknown-property.js

300 lines
9.8 KiB
JavaScript
Raw Normal View History

2022-04-25 02:47:15 +00:00
/**
* @fileoverview Prevent usage of unknown DOM property
* @author Yannick Croissant
*/
'use strict';
const has = require('has');
const docsUrl = require('../util/docsUrl');
const versionUtil = require('../util/version');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const DEFAULTS = {
ignore: []
};
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', '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'
];
function getDOMPropertyNames(context) {
// this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
if (!versionUtil.testReactVersion(context, '16.1.0')) {
return ['allowTransparency'].concat(DOM_PROPERTY_NAMES);
}
return DOM_PROPERTY_NAMES;
}
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* Checks if a node matches the JSX tag convention. This also checks if a node
* is extended as a webcomponent using the attribute "is".
* @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.
* @param {String} context - eslint context
* @returns {String | undefined} The standard name of the attribute, or undefined if no standard name was found.
*/
function getStandardName(name, context) {
if (has(DOM_ATTRIBUTE_NAMES, name)) {
return DOM_ATTRIBUTE_NAMES[name];
}
if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
return SVGDOM_ATTRIBUTE_NAMES[name];
}
const names = getDOMPropertyNames(context);
// Let's find a possible attribute match with a case-insensitive search.
return names.find((element) => element.toLowerCase() === name.toLowerCase());
}
// ------------------------------------------------------------------------------
// 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',
messages: {
invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
unknownProp: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead'
},
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);
// 1. Some attributes are allowed on some tags only.
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name) ? ATTRIBUTE_TAGS_MAP[name] : null;
if (tagName && allowedTags && /[^A-Z]/.test(tagName.charAt(0)) && allowedTags.indexOf(tagName) === -1) {
context.report({
node,
messageId: 'invalidPropOnTag',
data: {
name,
tagName,
allowedTags: allowedTags.join(', ')
}
});
}
// 2. Otherwise, we'll try to find if the attribute is a close version
// of what we should normally have with React. If yes, we'll report an
// error. We don't want to report if the input attribute name is the
// standard name though!
const standardName = getStandardName(name, context);
if (!isTagName(node) || !standardName || standardName === name) {
return;
}
context.report({
node,
messageId: 'unknownProp',
data: {
name,
standardName
},
fix(fixer) {
return fixer.replaceText(node.name, standardName);
}
});
}
};
}
};