paradiego
This commit is contained in:
426
node_modules/eslint-plugin-react/lib/rules/boolean-prop-naming.js
generated
vendored
Normal file
426
node_modules/eslint-plugin-react/lib/rules/boolean-prop-naming.js
generated
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* @fileoverview Enforces consistent naming for boolean props
|
||||
* @author Ev Haus
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const flatMap = require('array.prototype.flatmap');
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const propsUtil = require('../util/props');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
/**
|
||||
* Checks if prop is nested
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function nestedPropTypes(prop) {
|
||||
return (
|
||||
prop.type === 'Property'
|
||||
&& astUtil.isCallExpression(prop.value)
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
patternMismatch: 'Prop name `{{propName}}` doesn’t match rule `{{pattern}}`',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
category: 'Stylistic Issues',
|
||||
description: 'Enforces consistent naming for boolean props',
|
||||
recommended: false,
|
||||
url: docsUrl('boolean-prop-naming'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
propTypeNames: {
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
minItems: 1,
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
},
|
||||
rule: {
|
||||
default: '^(is|has)[A-Z]([A-Za-z0-9]?)+',
|
||||
minLength: 1,
|
||||
type: 'string',
|
||||
},
|
||||
message: {
|
||||
minLength: 1,
|
||||
type: 'string',
|
||||
},
|
||||
validateNested: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const config = context.options[0] || {};
|
||||
const rule = config.rule ? new RegExp(config.rule) : null;
|
||||
const propTypeNames = config.propTypeNames || ['bool'];
|
||||
|
||||
// Remembers all Flowtype object definitions
|
||||
const objectTypeAnnotations = new Map();
|
||||
|
||||
/**
|
||||
* Returns the prop key to ensure we handle the following cases:
|
||||
* propTypes: {
|
||||
* full: React.PropTypes.bool,
|
||||
* short: PropTypes.bool,
|
||||
* direct: bool,
|
||||
* required: PropTypes.bool.isRequired
|
||||
* }
|
||||
* @param {Object} node The node we're getting the name of
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function getPropKey(node) {
|
||||
// Check for `ExperimentalSpreadProperty` (eslint 3/4) and `SpreadElement` (eslint 5)
|
||||
// so we can skip validation of those fields.
|
||||
// Otherwise it will look for `node.value.property` which doesn't exist and breaks eslint.
|
||||
if (node.type === 'ExperimentalSpreadProperty' || node.type === 'SpreadElement') {
|
||||
return null;
|
||||
}
|
||||
if (node.value && node.value.property) {
|
||||
const name = node.value.property.name;
|
||||
if (name === 'isRequired') {
|
||||
if (node.value.object && node.value.object.property) {
|
||||
return node.value.object.property.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
if (node.value && node.value.type === 'Identifier') {
|
||||
return node.value.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the given node (prop)
|
||||
* @param {Object} node The node we're getting the name of
|
||||
* @returns {string}
|
||||
*/
|
||||
function getPropName(node) {
|
||||
// Due to this bug https://github.com/babel/babel-eslint/issues/307
|
||||
// we can't get the name of the Flow object key name. So we have
|
||||
// to hack around it for now.
|
||||
if (node.type === 'ObjectTypeProperty') {
|
||||
return getSourceCode(context).getFirstToken(node).value;
|
||||
}
|
||||
|
||||
return node.key.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if prop is declared in flow way
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function flowCheck(prop) {
|
||||
return (
|
||||
prop.type === 'ObjectTypeProperty'
|
||||
&& prop.value.type === 'BooleanTypeAnnotation'
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if prop is declared in regular way
|
||||
* @param {Object} prop Property object, single prop type declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function regularCheck(prop) {
|
||||
const propKey = getPropKey(prop);
|
||||
return (
|
||||
propKey
|
||||
&& propTypeNames.indexOf(propKey) >= 0
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
function tsCheck(prop) {
|
||||
if (prop.type !== 'TSPropertySignature') return false;
|
||||
const typeAnnotation = (prop.typeAnnotation || {}).typeAnnotation;
|
||||
return (
|
||||
typeAnnotation
|
||||
&& typeAnnotation.type === 'TSBooleanKeyword'
|
||||
&& rule.test(getPropName(prop)) === false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs recursive check on all proptypes
|
||||
* @param {Array} proptypes A list of Property object (for each proptype defined)
|
||||
* @param {Function} addInvalidProp callback to run for each error
|
||||
*/
|
||||
function runCheck(proptypes, addInvalidProp) {
|
||||
if (proptypes) {
|
||||
proptypes.forEach((prop) => {
|
||||
if (config.validateNested && nestedPropTypes(prop)) {
|
||||
runCheck(prop.value.arguments[0].properties, addInvalidProp);
|
||||
return;
|
||||
}
|
||||
if (flowCheck(prop) || regularCheck(prop) || tsCheck(prop)) {
|
||||
addInvalidProp(prop);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and mark props with invalid naming
|
||||
* @param {Object} node The component node we're testing
|
||||
* @param {Array} proptypes A list of Property object (for each proptype defined)
|
||||
*/
|
||||
function validatePropNaming(node, proptypes) {
|
||||
const component = components.get(node) || node;
|
||||
const invalidProps = component.invalidProps || [];
|
||||
|
||||
runCheck(proptypes, (prop) => {
|
||||
invalidProps.push(prop);
|
||||
});
|
||||
|
||||
components.set(node, {
|
||||
invalidProps,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports invalid prop naming
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportInvalidNaming(component) {
|
||||
component.invalidProps.forEach((propNode) => {
|
||||
const propName = getPropName(propNode);
|
||||
report(context, config.message || messages.patternMismatch, !config.message && 'patternMismatch', {
|
||||
node: propNode,
|
||||
data: {
|
||||
component: propName,
|
||||
propName,
|
||||
pattern: config.rule,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkPropWrapperArguments(node, args) {
|
||||
if (!node || !Array.isArray(args)) {
|
||||
return;
|
||||
}
|
||||
args.filter((arg) => arg.type === 'ObjectExpression').forEach((object) => validatePropNaming(node, object.properties));
|
||||
}
|
||||
|
||||
function getComponentTypeAnnotation(component) {
|
||||
// If this is a functional component that uses a global type, check it
|
||||
if (
|
||||
(component.node.type === 'FunctionDeclaration' || component.node.type === 'ArrowFunctionExpression')
|
||||
&& component.node.params
|
||||
&& component.node.params.length > 0
|
||||
&& component.node.params[0].typeAnnotation
|
||||
) {
|
||||
return component.node.params[0].typeAnnotation.typeAnnotation;
|
||||
}
|
||||
|
||||
if (
|
||||
!component.node.parent
|
||||
|| component.node.parent.type !== 'VariableDeclarator'
|
||||
|| !component.node.parent.id
|
||||
|| component.node.parent.id.type !== 'Identifier'
|
||||
|| !component.node.parent.id.typeAnnotation
|
||||
|| !component.node.parent.id.typeAnnotation.typeAnnotation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const annotationTypeArguments = propsUtil.getTypeArguments(
|
||||
component.node.parent.id.typeAnnotation.typeAnnotation
|
||||
);
|
||||
if (
|
||||
annotationTypeArguments && (
|
||||
annotationTypeArguments.type === 'TSTypeParameterInstantiation'
|
||||
|| annotationTypeArguments.type === 'TypeParameterInstantiation'
|
||||
)
|
||||
) {
|
||||
return annotationTypeArguments.params.find(
|
||||
(param) => param.type === 'TSTypeReference' || param.type === 'GenericTypeAnnotation'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findAllTypeAnnotations(identifier, node) {
|
||||
if (node.type === 'TSTypeLiteral' || node.type === 'ObjectTypeAnnotation' || node.type === 'TSInterfaceBody') {
|
||||
const currentNode = [].concat(
|
||||
objectTypeAnnotations.get(identifier.name) || [],
|
||||
node
|
||||
);
|
||||
objectTypeAnnotations.set(identifier.name, currentNode);
|
||||
} else if (
|
||||
node.type === 'TSParenthesizedType'
|
||||
&& (
|
||||
node.typeAnnotation.type === 'TSIntersectionType'
|
||||
|| node.typeAnnotation.type === 'TSUnionType'
|
||||
)
|
||||
) {
|
||||
node.typeAnnotation.types.forEach((type) => {
|
||||
findAllTypeAnnotations(identifier, type);
|
||||
});
|
||||
} else if (
|
||||
node.type === 'TSIntersectionType'
|
||||
|| node.type === 'TSUnionType'
|
||||
|| node.type === 'IntersectionTypeAnnotation'
|
||||
|| node.type === 'UnionTypeAnnotation'
|
||||
) {
|
||||
node.types.forEach((type) => {
|
||||
findAllTypeAnnotations(identifier, type);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
node.value
|
||||
&& astUtil.isCallExpression(node.value)
|
||||
&& propWrapperUtil.isPropWrapperFunction(
|
||||
context,
|
||||
getText(context, node.value.callee)
|
||||
)
|
||||
) {
|
||||
checkPropWrapperArguments(node, node.value.arguments);
|
||||
}
|
||||
if (node.value && node.value.properties) {
|
||||
validatePropNaming(node, node.value.properties);
|
||||
}
|
||||
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
|
||||
validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
|
||||
}
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!rule || !propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component || !node.parent.right) {
|
||||
return;
|
||||
}
|
||||
const right = node.parent.right;
|
||||
if (
|
||||
astUtil.isCallExpression(right)
|
||||
&& propWrapperUtil.isPropWrapperFunction(
|
||||
context,
|
||||
getText(context, right.callee)
|
||||
)
|
||||
) {
|
||||
checkPropWrapperArguments(component.node, right.arguments);
|
||||
return;
|
||||
}
|
||||
validatePropNaming(component.node, node.parent.right.properties);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the proptypes declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (!propsUtil.isPropTypesDeclaration(property)) {
|
||||
return;
|
||||
}
|
||||
validatePropNaming(node, property.value.properties);
|
||||
});
|
||||
},
|
||||
|
||||
TypeAlias(node) {
|
||||
findAllTypeAnnotations(node.id, node.right);
|
||||
},
|
||||
|
||||
TSTypeAliasDeclaration(node) {
|
||||
findAllTypeAnnotations(node.id, node.typeAnnotation);
|
||||
},
|
||||
|
||||
TSInterfaceDeclaration(node) {
|
||||
findAllTypeAnnotations(node.id, node.body);
|
||||
},
|
||||
|
||||
// eslint-disable-next-line object-shorthand
|
||||
'Program:exit'() {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
values(components.list()).forEach((component) => {
|
||||
const annotation = getComponentTypeAnnotation(component);
|
||||
|
||||
if (annotation) {
|
||||
let propType;
|
||||
if (annotation.type === 'GenericTypeAnnotation') {
|
||||
propType = objectTypeAnnotations.get(annotation.id.name);
|
||||
} else if (annotation.type === 'ObjectTypeAnnotation' || annotation.type === 'TSTypeLiteral') {
|
||||
propType = annotation;
|
||||
} else if (annotation.type === 'TSTypeReference') {
|
||||
propType = objectTypeAnnotations.get(annotation.typeName.name);
|
||||
} else if (annotation.type === 'TSIntersectionType') {
|
||||
propType = flatMap(annotation.types, (type) => (
|
||||
type.type === 'TSTypeReference'
|
||||
? objectTypeAnnotations.get(type.typeName.name)
|
||||
: type
|
||||
));
|
||||
}
|
||||
|
||||
if (propType) {
|
||||
[].concat(propType).filter(Boolean).forEach((prop) => {
|
||||
validatePropNaming(
|
||||
component.node,
|
||||
prop.properties || prop.members || prop.body
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (component.invalidProps && component.invalidProps.length > 0) {
|
||||
reportInvalidNaming(component);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset cache
|
||||
objectTypeAnnotations.clear();
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
169
node_modules/eslint-plugin-react/lib/rules/button-has-type.js
generated
vendored
Normal file
169
node_modules/eslint-plugin-react/lib/rules/button-has-type.js
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @fileoverview Forbid "button" element without an explicit "type" attribute
|
||||
* @author Filipp Riabchun
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const getProp = require('jsx-ast-utils/getProp');
|
||||
const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const optionDefaults = {
|
||||
button: true,
|
||||
submit: true,
|
||||
reset: true,
|
||||
};
|
||||
|
||||
const messages = {
|
||||
missingType: 'Missing an explicit type attribute for button',
|
||||
complexType: 'The button type attribute must be specified by a static string or a trivial ternary expression',
|
||||
invalidValue: '"{{value}}" is an invalid value for button type attribute',
|
||||
forbiddenValue: '"{{value}}" is an invalid value for button type attribute',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of `button` elements without an explicit `type` attribute',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('button-has-type'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
button: {
|
||||
default: optionDefaults.button,
|
||||
type: 'boolean',
|
||||
},
|
||||
submit: {
|
||||
default: optionDefaults.submit,
|
||||
type: 'boolean',
|
||||
},
|
||||
reset: {
|
||||
default: optionDefaults.reset,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = Object.assign({}, optionDefaults, context.options[0]);
|
||||
|
||||
function reportMissing(node) {
|
||||
report(context, messages.missingType, 'missingType', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
function reportComplex(node) {
|
||||
report(context, messages.complexType, 'complexType', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
function checkValue(node, value) {
|
||||
if (!(value in configuration)) {
|
||||
report(context, messages.invalidValue, 'invalidValue', {
|
||||
node,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
} else if (!configuration[value]) {
|
||||
report(context, messages.forbiddenValue, 'forbiddenValue', {
|
||||
node,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkExpression(node, expression) {
|
||||
switch (expression.type) {
|
||||
case 'Literal':
|
||||
checkValue(node, expression.value);
|
||||
return;
|
||||
case 'TemplateLiteral':
|
||||
if (expression.expressions.length === 0) {
|
||||
checkValue(node, expression.quasis[0].value.raw);
|
||||
} else {
|
||||
reportComplex(expression);
|
||||
}
|
||||
return;
|
||||
case 'ConditionalExpression':
|
||||
checkExpression(node, expression.consequent);
|
||||
checkExpression(node, expression.alternate);
|
||||
return;
|
||||
default:
|
||||
reportComplex(expression);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (node.openingElement.name.name !== 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeProp = getProp(node.openingElement.attributes, 'type');
|
||||
|
||||
if (!typeProp) {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeProp.value && typeProp.value.type === 'JSXExpressionContainer') {
|
||||
checkExpression(node, typeProp.value.expression);
|
||||
return;
|
||||
}
|
||||
|
||||
const propValue = getLiteralPropValue(typeProp);
|
||||
checkValue(node, propValue);
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node) || node.arguments.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.arguments[0].type !== 'Literal' || node.arguments[0].value !== 'button') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.arguments[1] || node.arguments[1].type !== 'ObjectExpression') {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const props = node.arguments[1].properties;
|
||||
const typeProp = props.find((prop) => (
|
||||
'key' in prop
|
||||
&& prop.key
|
||||
&& 'name' in prop.key
|
||||
&& prop.key.name === 'type'
|
||||
));
|
||||
|
||||
if (!typeProp) {
|
||||
reportMissing(node);
|
||||
return;
|
||||
}
|
||||
|
||||
checkExpression(node, 'value' in typeProp ? typeProp.value : undefined);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
142
node_modules/eslint-plugin-react/lib/rules/checked-requires-onchange-or-readonly.js
generated
vendored
Normal file
142
node_modules/eslint-plugin-react/lib/rules/checked-requires-onchange-or-readonly.js
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview Enforce the use of the 'onChange' or 'readonly' attribute when 'checked' is used'
|
||||
* @author Jaesoekjjang
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const ASTUtils = require('jsx-ast-utils');
|
||||
const flatMap = require('array.prototype.flatmap');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
|
||||
const messages = {
|
||||
missingProperty: '`checked` should be used with either `onChange` or `readOnly`.',
|
||||
exclusiveCheckedAttribute: 'Use either `checked` or `defaultChecked`, but not both.',
|
||||
};
|
||||
|
||||
const targetPropSet = new Set(['checked', 'onChange', 'readOnly', 'defaultChecked']);
|
||||
|
||||
const defaultOptions = {
|
||||
ignoreMissingProperties: false,
|
||||
ignoreExclusiveCheckedAttribute: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object[]} properties
|
||||
* @param {string} keyName
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function extractTargetProps(properties, keyName) {
|
||||
return new Set(
|
||||
flatMap(
|
||||
properties,
|
||||
(prop) => (
|
||||
prop[keyName] && targetPropSet.has(prop[keyName].name)
|
||||
? [prop[keyName].name]
|
||||
: []
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce using `onChange` or `readonly` attribute when `checked` is used',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('checked-requires-onchange-or-readonly'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
ignoreMissingProperties: {
|
||||
type: 'boolean',
|
||||
},
|
||||
ignoreExclusiveCheckedAttribute: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
create(context) {
|
||||
const options = Object.assign({}, defaultOptions, context.options[0]);
|
||||
|
||||
function reportMissingProperty(node) {
|
||||
report(
|
||||
context,
|
||||
messages.missingProperty,
|
||||
'missingProperty',
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
function reportExclusiveCheckedAttribute(node) {
|
||||
report(
|
||||
context,
|
||||
messages.exclusiveCheckedAttribute,
|
||||
'exclusiveCheckedAttribute',
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @param {Set<string>} propSet
|
||||
* @returns {void}
|
||||
*/
|
||||
const checkAttributesAndReport = (node, propSet) => {
|
||||
if (!propSet.has('checked')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.ignoreExclusiveCheckedAttribute && propSet.has('defaultChecked')) {
|
||||
reportExclusiveCheckedAttribute(node);
|
||||
}
|
||||
|
||||
if (
|
||||
!options.ignoreMissingProperties
|
||||
&& !(propSet.has('onChange') || propSet.has('readOnly'))
|
||||
) {
|
||||
reportMissingProperty(node);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (ASTUtils.elementType(node) !== 'input') {
|
||||
return;
|
||||
}
|
||||
|
||||
const propSet = extractTargetProps(node.attributes, 'name');
|
||||
checkAttributesAndReport(node, propSet);
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0];
|
||||
const secondArg = node.arguments[1];
|
||||
if (
|
||||
!firstArg
|
||||
|| firstArg.type !== 'Literal'
|
||||
|| firstArg.value !== 'input'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secondArg || secondArg.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
const propSet = extractTargetProps(secondArg.properties, 'key');
|
||||
checkAttributesAndReport(node, propSet);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
109
node_modules/eslint-plugin-react/lib/rules/default-props-match-prop-types.js
generated
vendored
Normal file
109
node_modules/eslint-plugin-react/lib/rules/default-props-match-prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @fileOverview Enforce all defaultProps are defined in propTypes
|
||||
* @author Vitor Balocco
|
||||
* @author Roy Sutton
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
requiredHasDefault: 'defaultProp "{{name}}" defined for isRequired propType.',
|
||||
defaultHasNoType: 'defaultProp "{{name}}" has no corresponding propTypes declaration.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce all defaultProps have a corresponding non-required PropType',
|
||||
category: 'Best Practices',
|
||||
url: docsUrl('default-props-match-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowRequiredDefaults: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowRequiredDefaults = configuration.allowRequiredDefaults || false;
|
||||
|
||||
/**
|
||||
* Reports all defaultProps passed in that don't have an appropriate propTypes counterpart.
|
||||
* @param {Object[]} propTypes Array of propTypes to check.
|
||||
* @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
|
||||
* @return {void}
|
||||
*/
|
||||
function reportInvalidDefaultProps(propTypes, defaultProps) {
|
||||
// If this defaultProps is "unresolved" or the propTypes is undefined, then we should ignore
|
||||
// this component and not report any errors for it, to avoid false-positives with e.g.
|
||||
// external defaultProps/propTypes declarations or spread operators.
|
||||
if (defaultProps === 'unresolved' || !propTypes || Object.keys(propTypes).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(defaultProps).forEach((defaultPropName) => {
|
||||
const defaultProp = defaultProps[defaultPropName];
|
||||
const prop = propTypes[defaultPropName];
|
||||
|
||||
if (prop && (allowRequiredDefaults || !prop.isRequired)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
report(context, messages.requiredHasDefault, 'requiredHasDefault', {
|
||||
node: defaultProp.node,
|
||||
data: {
|
||||
name: defaultPropName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
report(context, messages.defaultHasNoType, 'defaultHasNoType', {
|
||||
node: defaultProp.node,
|
||||
data: {
|
||||
name: defaultPropName,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
// If no defaultProps could be found, we don't report anything.
|
||||
values(components.list())
|
||||
.filter((component) => component.defaultProps)
|
||||
.forEach((component) => {
|
||||
reportInvalidDefaultProps(
|
||||
component.declaredPropTypes,
|
||||
component.defaultProps || {}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
285
node_modules/eslint-plugin-react/lib/rules/destructuring-assignment.js
generated
vendored
Normal file
285
node_modules/eslint-plugin-react/lib/rules/destructuring-assignment.js
generated
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Enforce consistent usage of destructuring assignment of props, state, and context.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const isAssignmentLHS = require('../util/ast').isAssignmentLHS;
|
||||
const report = require('../util/report');
|
||||
|
||||
const getScope = eslintUtil.getScope;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
const DEFAULT_OPTION = 'always';
|
||||
|
||||
function createSFCParams() {
|
||||
const queue = [];
|
||||
|
||||
return {
|
||||
push(params) {
|
||||
queue.unshift(params);
|
||||
},
|
||||
pop() {
|
||||
queue.shift();
|
||||
},
|
||||
propsName() {
|
||||
const found = queue.find((params) => {
|
||||
const props = params[0];
|
||||
return props && !props.destructuring && props.name;
|
||||
});
|
||||
return found && found[0] && found[0].name;
|
||||
},
|
||||
contextName() {
|
||||
const found = queue.find((params) => {
|
||||
const context = params[1];
|
||||
return context && !context.destructuring && context.name;
|
||||
});
|
||||
return found && found[1] && found[1].name;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function evalParams(params) {
|
||||
return params.map((param) => ({
|
||||
destructuring: param.type === 'ObjectPattern',
|
||||
name: param.type === 'Identifier' && param.name,
|
||||
}));
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noDestructPropsInSFCArg: 'Must never use destructuring props assignment in SFC argument',
|
||||
noDestructContextInSFCArg: 'Must never use destructuring context assignment in SFC argument',
|
||||
noDestructAssignment: 'Must never use destructuring {{type}} assignment',
|
||||
useDestructAssignment: 'Must use destructuring {{type}} assignment',
|
||||
destructureInSignature: 'Must destructure props in the function signature.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce consistent usage of destructuring assignment of props, state, and context',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('destructuring-assignment'),
|
||||
},
|
||||
fixable: 'code',
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'string',
|
||||
enum: [
|
||||
'always',
|
||||
'never',
|
||||
],
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreClassFields: {
|
||||
type: 'boolean',
|
||||
},
|
||||
destructureInSignature: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'always',
|
||||
'ignore',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const configuration = context.options[0] || DEFAULT_OPTION;
|
||||
const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false;
|
||||
const destructureInSignature = (context.options[1] && context.options[1].destructureInSignature) || 'ignore';
|
||||
const sfcParams = createSFCParams();
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node We expect either an ArrowFunctionExpression,
|
||||
* FunctionDeclaration, or FunctionExpression
|
||||
*/
|
||||
function handleStatelessComponent(node) {
|
||||
const params = evalParams(node.params);
|
||||
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
if (!SFCComponent) {
|
||||
return;
|
||||
}
|
||||
sfcParams.push(params);
|
||||
|
||||
if (params[0] && params[0].destructuring && components.get(node) && configuration === 'never') {
|
||||
report(context, messages.noDestructPropsInSFCArg, 'noDestructPropsInSFCArg', {
|
||||
node,
|
||||
});
|
||||
} else if (params[1] && params[1].destructuring && components.get(node) && configuration === 'never') {
|
||||
report(context, messages.noDestructContextInSFCArg, 'noDestructContextInSFCArg', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleStatelessComponentExit(node) {
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
if (SFCComponent) {
|
||||
sfcParams.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSFCUsage(node) {
|
||||
const propsName = sfcParams.propsName();
|
||||
const contextName = sfcParams.contextName();
|
||||
// props.aProp || context.aProp
|
||||
const isPropUsed = (
|
||||
(propsName && node.object.name === propsName)
|
||||
|| (contextName && node.object.name === contextName)
|
||||
)
|
||||
&& !isAssignmentLHS(node);
|
||||
if (isPropUsed && configuration === 'always' && !node.optional) {
|
||||
report(context, messages.useDestructAssignment, 'useDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.object.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isInClassProperty(node) {
|
||||
let curNode = node.parent;
|
||||
while (curNode) {
|
||||
if (curNode.type === 'ClassProperty' || curNode.type === 'PropertyDefinition') {
|
||||
return true;
|
||||
}
|
||||
curNode = curNode.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleClassUsage(node) {
|
||||
// this.props.Aprop || this.context.aProp || this.state.aState
|
||||
const isPropUsed = (
|
||||
node.object.type === 'MemberExpression' && node.object.object.type === 'ThisExpression'
|
||||
&& (node.object.property.name === 'props' || node.object.property.name === 'context' || node.object.property.name === 'state')
|
||||
&& !isAssignmentLHS(node)
|
||||
);
|
||||
|
||||
if (
|
||||
isPropUsed && configuration === 'always'
|
||||
&& !(ignoreClassFields && isInClassProperty(node))
|
||||
) {
|
||||
report(context, messages.useDestructAssignment, 'useDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.object.property.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
FunctionDeclaration: handleStatelessComponent,
|
||||
|
||||
ArrowFunctionExpression: handleStatelessComponent,
|
||||
|
||||
FunctionExpression: handleStatelessComponent,
|
||||
|
||||
'FunctionDeclaration:exit': handleStatelessComponentExit,
|
||||
|
||||
'ArrowFunctionExpression:exit': handleStatelessComponentExit,
|
||||
|
||||
'FunctionExpression:exit': handleStatelessComponentExit,
|
||||
|
||||
MemberExpression(node) {
|
||||
let scope = getScope(context, node);
|
||||
let SFCComponent = components.get(scope.block);
|
||||
while (!SFCComponent && scope.upper && scope.upper !== scope) {
|
||||
SFCComponent = components.get(scope.upper.block);
|
||||
scope = scope.upper;
|
||||
}
|
||||
if (SFCComponent) {
|
||||
handleSFCUsage(node);
|
||||
}
|
||||
|
||||
const classComponent = utils.getParentComponent(node);
|
||||
if (classComponent) {
|
||||
handleClassUsage(node);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
const classComponent = utils.getParentComponent(node);
|
||||
const SFCComponent = components.get(getScope(context, node).block);
|
||||
|
||||
const destructuring = (node.init && node.id && node.id.type === 'ObjectPattern');
|
||||
// let {foo} = props;
|
||||
const destructuringSFC = destructuring && (node.init.name === 'props' || node.init.name === 'context');
|
||||
// let {foo} = this.props;
|
||||
const destructuringClass = destructuring && node.init.object && node.init.object.type === 'ThisExpression' && (
|
||||
node.init.property.name === 'props' || node.init.property.name === 'context' || node.init.property.name === 'state'
|
||||
);
|
||||
|
||||
if (SFCComponent && destructuringSFC && configuration === 'never') {
|
||||
report(context, messages.noDestructAssignment, 'noDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.init.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
classComponent && destructuringClass && configuration === 'never'
|
||||
&& !(ignoreClassFields && (node.parent.type === 'ClassProperty' || node.parent.type === 'PropertyDefinition'))
|
||||
) {
|
||||
report(context, messages.noDestructAssignment, 'noDestructAssignment', {
|
||||
node,
|
||||
data: {
|
||||
type: node.init.property.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
SFCComponent
|
||||
&& destructuringSFC
|
||||
&& configuration === 'always'
|
||||
&& destructureInSignature === 'always'
|
||||
&& node.init.name === 'props'
|
||||
) {
|
||||
const scopeSetProps = getScope(context, node).set.get('props');
|
||||
const propsRefs = scopeSetProps && scopeSetProps.references;
|
||||
if (!propsRefs) {
|
||||
return;
|
||||
}
|
||||
// Skip if props is used elsewhere
|
||||
if (propsRefs.length > 1) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.destructureInSignature, 'destructureInSignature', {
|
||||
node,
|
||||
fix(fixer) {
|
||||
const param = SFCComponent.node.params[0];
|
||||
if (!param) {
|
||||
return;
|
||||
}
|
||||
const replaceRange = [
|
||||
param.range[0],
|
||||
param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1],
|
||||
];
|
||||
return [
|
||||
fixer.replaceTextRange(replaceRange, getText(context, node.id)),
|
||||
fixer.remove(node.parent),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
285
node_modules/eslint-plugin-react/lib/rules/display-name.js
generated
vendored
Normal file
285
node_modules/eslint-plugin-react/lib/rules/display-name.js
generated
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Prevent missing displayName in a React component definition
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
const filter = require('es-iterator-helpers/Iterator.prototype.filter');
|
||||
const forEach = require('es-iterator-helpers/Iterator.prototype.forEach');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const isCreateContext = require('../util/isCreateContext');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const propsUtil = require('../util/props');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noDisplayName: 'Component definition is missing display name',
|
||||
noContextDisplayName: 'Context definition is missing display name',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing displayName in a React component definition',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('display-name'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreTranspilerName: {
|
||||
type: 'boolean',
|
||||
},
|
||||
checkContextObjects: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const config = context.options[0] || {};
|
||||
const ignoreTranspilerName = config.ignoreTranspilerName || false;
|
||||
const checkContextObjects = (config.checkContextObjects || false) && testReactVersion(context, '>= 16.3.0');
|
||||
|
||||
const contextObjects = new Map();
|
||||
|
||||
/**
|
||||
* Mark a prop type as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markDisplayNameAsDeclared(node) {
|
||||
components.set(node, {
|
||||
hasDisplayName: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if React.forwardRef is nested inside React.memo
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if React.forwardRef is nested inside React.memo, false if not.
|
||||
*/
|
||||
function isNestedMemo(node) {
|
||||
return astUtil.isCallExpression(node)
|
||||
&& node.arguments
|
||||
&& astUtil.isCallExpression(node.arguments[0])
|
||||
&& utils.isPragmaComponentWrapper(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports missing display name for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportMissingDisplayName(component) {
|
||||
if (
|
||||
testReactVersion(context, '^0.14.10 || ^15.7.0 || >= 16.12.0')
|
||||
&& isNestedMemo(component.node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.noDisplayName, 'noDisplayName', {
|
||||
node: component.node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports missing display name for a given context object
|
||||
* @param {Object} contextObj The context object to process
|
||||
*/
|
||||
function reportMissingContextDisplayName(contextObj) {
|
||||
report(context, messages.noContextDisplayName, 'noContextDisplayName', {
|
||||
node: contextObj.node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the component have a name set by the transpiler
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if component has a name, false if not.
|
||||
*/
|
||||
function hasTranspilerName(node) {
|
||||
const namedObjectAssignment = (
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'AssignmentExpression'
|
||||
&& (
|
||||
!node.parent.parent.left.object
|
||||
|| node.parent.parent.left.object.name !== 'module'
|
||||
|| node.parent.parent.left.property.name !== 'exports'
|
||||
)
|
||||
);
|
||||
const namedObjectDeclaration = (
|
||||
node.type === 'ObjectExpression'
|
||||
&& node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'VariableDeclarator'
|
||||
);
|
||||
const namedClass = (
|
||||
(node.type === 'ClassDeclaration' || node.type === 'ClassExpression')
|
||||
&& node.id
|
||||
&& !!node.id.name
|
||||
);
|
||||
|
||||
const namedFunctionDeclaration = (
|
||||
(node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
|
||||
&& node.id
|
||||
&& !!node.id.name
|
||||
);
|
||||
|
||||
const namedFunctionExpression = (
|
||||
astUtil.isFunctionLikeExpression(node)
|
||||
&& node.parent
|
||||
&& (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true)
|
||||
&& (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context))
|
||||
);
|
||||
|
||||
if (
|
||||
namedObjectAssignment || namedObjectDeclaration
|
||||
|| namedClass
|
||||
|| namedFunctionDeclaration || namedFunctionExpression
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ExpressionStatement(node) {
|
||||
if (checkContextObjects && isCreateContext(node)) {
|
||||
contextObjects.set(node.expression.left.name, { node, hasDisplayName: false });
|
||||
}
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (checkContextObjects && isCreateContext(node)) {
|
||||
contextObjects.set(node.id.name, { node, hasDisplayName: false });
|
||||
}
|
||||
},
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node.property)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
checkContextObjects
|
||||
&& node.object
|
||||
&& node.object.name
|
||||
&& contextObjects.has(node.object.name)
|
||||
) {
|
||||
contextObjects.get(node.object.name).hasDisplayName = true;
|
||||
}
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(astUtil.unwrapTSAsExpression(component.node));
|
||||
},
|
||||
|
||||
'FunctionExpression, FunctionDeclaration, ArrowFunctionExpression'(node) {
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
return;
|
||||
}
|
||||
if (components.get(node)) {
|
||||
markDisplayNameAsDeclared(node);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (!propsUtil.isDisplayNameDeclaration(node.key)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
'ClassExpression, ClassDeclaration'(node) {
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (!componentUtil.isES5Component(node, context)) {
|
||||
return;
|
||||
}
|
||||
if (ignoreTranspilerName || !hasTranspilerName(node)) {
|
||||
// Search for the displayName declaration
|
||||
node.properties.forEach((property) => {
|
||||
if (!property.key || !propsUtil.isDisplayNameDeclaration(property.key)) {
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
});
|
||||
return;
|
||||
}
|
||||
markDisplayNameAsDeclared(node);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!utils.isPragmaComponentWrapper(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
||||
// Skip over React.forwardRef declarations that are embedded within
|
||||
// a React.memo i.e. React.memo(React.forwardRef(/* ... */))
|
||||
// This means that we raise a single error for the call to React.memo
|
||||
// instead of one for React.memo and one for React.forwardRef
|
||||
const isWrappedInAnotherPragma = utils.getPragmaComponentWrapper(node);
|
||||
if (
|
||||
!isWrappedInAnotherPragma
|
||||
&& (ignoreTranspilerName || !hasTranspilerName(node.arguments[0]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (components.get(node)) {
|
||||
markDisplayNameAsDeclared(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
// Report missing display name for all components
|
||||
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
|
||||
reportMissingDisplayName(component);
|
||||
});
|
||||
if (checkContextObjects) {
|
||||
// Report missing display name for all context objects
|
||||
forEach(
|
||||
filter(contextObjects.values(), (v) => !v.hasDisplayName),
|
||||
(contextObj) => reportMissingContextDisplayName(contextObj)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
180
node_modules/eslint-plugin-react/lib/rules/forbid-component-props.js
generated
vendored
Normal file
180
node_modules/eslint-plugin-react/lib/rules/forbid-component-props.js
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain props on components
|
||||
* @author Joe Lencioni
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const minimatch = require('minimatch');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = ['className', 'style'];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propIsForbidden: 'Prop "{{prop}}" is forbidden on Components',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain props on components',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-component-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: { type: 'string' },
|
||||
allowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: { type: 'string' },
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['disallowedFor'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propNamePattern: { type: 'string' },
|
||||
allowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
propNamePattern: { type: 'string' },
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['disallowedFor'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
|
||||
const propName = typeof value === 'string' ? value : value.propName;
|
||||
const propPattern = value.propNamePattern;
|
||||
const prop = propName || propPattern;
|
||||
const options = {
|
||||
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
|
||||
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
|
||||
message: typeof value === 'string' ? null : value.message,
|
||||
isPattern: !!value.propNamePattern,
|
||||
};
|
||||
return [prop, options];
|
||||
}));
|
||||
|
||||
function getPropOptions(prop) {
|
||||
// Get config options having pattern
|
||||
const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern);
|
||||
// Match current prop with pattern options, return if matched
|
||||
const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0]));
|
||||
// Get options for matched propNamePattern
|
||||
const propNamePatternOptions = propNamePattern && propNamePattern[1];
|
||||
|
||||
const options = forbid.get(prop) || propNamePatternOptions;
|
||||
return options;
|
||||
}
|
||||
|
||||
function isForbidden(prop, tagName) {
|
||||
const options = getPropOptions(prop);
|
||||
if (!options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallowList should have a least one item (schema configuration)
|
||||
const isTagForbidden = options.disallowList.length > 0
|
||||
? options.disallowList.indexOf(tagName) !== -1
|
||||
: options.allowList.indexOf(tagName) === -1;
|
||||
|
||||
// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
|
||||
return typeof tagName === 'undefined' || isTagForbidden;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const parentName = node.parent.name;
|
||||
// Extract a component name when using a "namespace", e.g. `<AntdLayout.Content />`.
|
||||
const tag = parentName.name || `${parentName.object.name}.${parentName.property.name}`;
|
||||
const componentName = parentName.name || parentName.property.name;
|
||||
if (componentName && typeof componentName[0] === 'string' && componentName[0] !== componentName[0].toUpperCase()) {
|
||||
// This is a DOM node, not a Component, so exit.
|
||||
return;
|
||||
}
|
||||
|
||||
const prop = node.name.name;
|
||||
|
||||
if (!isForbidden(prop, tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = getPropOptions(prop).message;
|
||||
|
||||
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
|
||||
node,
|
||||
data: {
|
||||
prop,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
122
node_modules/eslint-plugin-react/lib/rules/forbid-dom-props.js
generated
vendored
Normal file
122
node_modules/eslint-plugin-react/lib/rules/forbid-dom-props.js
generated
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain props on DOM Nodes
|
||||
* @author David Vázquez
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = [];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {Map<string, object>} forbidMap // { disallowList: null | string[], message: null | string }
|
||||
* @param {string} prop
|
||||
* @param {string} tagName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isForbidden(forbidMap, prop, tagName) {
|
||||
const options = forbidMap.get(prop);
|
||||
return options && (
|
||||
typeof tagName === 'undefined'
|
||||
|| !options.disallowList
|
||||
|| options.disallowList.indexOf(tagName) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain props on DOM Nodes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-dom-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [{
|
||||
type: 'string',
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
propName: {
|
||||
type: 'string',
|
||||
},
|
||||
disallowedFor: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}],
|
||||
minLength: 1,
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
|
||||
const propName = typeof value === 'string' ? value : value.propName;
|
||||
return [propName, {
|
||||
disallowList: typeof value === 'string' ? null : (value.disallowedFor || null),
|
||||
message: typeof value === 'string' ? null : value.message,
|
||||
}];
|
||||
}));
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const tag = node.parent.name.name;
|
||||
if (!(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())) {
|
||||
// This is a Component, not a DOM node, so exit.
|
||||
return;
|
||||
}
|
||||
|
||||
const prop = node.name.name;
|
||||
|
||||
if (!isForbidden(forbid, prop, tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customMessage = forbid.get(prop).message;
|
||||
|
||||
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
|
||||
node,
|
||||
data: {
|
||||
prop,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
119
node_modules/eslint-plugin-react/lib/rules/forbid-elements.js
generated
vendored
Normal file
119
node_modules/eslint-plugin-react/lib/rules/forbid-elements.js
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain elements
|
||||
* @author Kenneth Chung
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
forbiddenElement: '<{{element}}> is forbidden',
|
||||
forbiddenElement_message: '<{{element}}> is forbidden, {{message}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain elements',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-elements'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
element: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
required: ['element'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbidConfiguration = configuration.forbid || [];
|
||||
|
||||
/** @type {Record<string, { element: string, message?: string }>} */
|
||||
const indexedForbidConfigs = {};
|
||||
|
||||
forbidConfiguration.forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
indexedForbidConfigs[item] = { element: item };
|
||||
} else {
|
||||
indexedForbidConfigs[item.element] = item;
|
||||
}
|
||||
});
|
||||
|
||||
function reportIfForbidden(element, node) {
|
||||
if (has(indexedForbidConfigs, element)) {
|
||||
const message = indexedForbidConfigs[element].message;
|
||||
|
||||
report(
|
||||
context,
|
||||
message ? messages.forbiddenElement_message : messages.forbiddenElement,
|
||||
message ? 'forbiddenElement_message' : 'forbiddenElement',
|
||||
{
|
||||
node,
|
||||
data: {
|
||||
element,
|
||||
message,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
reportIfForbidden(getText(context, node.name), node.name);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const argument = node.arguments[0];
|
||||
if (!argument) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argument.type === 'Identifier' && /^[A-Z_]/.test(argument.name)) {
|
||||
reportIfForbidden(argument.name, argument);
|
||||
} else if (argument.type === 'Literal' && /^[a-z][^.]*$/.test(String(argument.value))) {
|
||||
reportIfForbidden(argument.value, argument);
|
||||
} else if (argument.type === 'MemberExpression') {
|
||||
reportIfForbidden(getText(context, argument), argument);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
141
node_modules/eslint-plugin-react/lib/rules/forbid-foreign-prop-types.js
generated
vendored
Normal file
141
node_modules/eslint-plugin-react/lib/rules/forbid-foreign-prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @fileoverview Forbid using another component's propTypes
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const ast = require('../util/ast');
|
||||
const report = require('../util/report');
|
||||
|
||||
const messages = {
|
||||
forbiddenPropType: 'Using propTypes from another component is not safe because they may be removed in production builds',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow using another component\'s propTypes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-foreign-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowInPropTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const allowInPropTypes = config.allowInPropTypes || false;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
function findParentAssignmentExpression(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && parent.type !== 'Program') {
|
||||
if (parent.type === 'AssignmentExpression') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findParentClassProperty(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
while (parent && parent.type !== 'Program') {
|
||||
if (parent.type === 'ClassProperty' || parent.type === 'PropertyDefinition') {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAllowedAssignment(node) {
|
||||
if (!allowInPropTypes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const assignmentExpression = findParentAssignmentExpression(node);
|
||||
|
||||
if (
|
||||
assignmentExpression
|
||||
&& assignmentExpression.left
|
||||
&& assignmentExpression.left.property
|
||||
&& assignmentExpression.left.property.name === 'propTypes'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const classProperty = findParentClassProperty(node);
|
||||
|
||||
if (
|
||||
classProperty
|
||||
&& classProperty.key
|
||||
&& classProperty.key.name === 'propTypes'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
(node.property
|
||||
&& (
|
||||
!node.computed
|
||||
&& node.property.type === 'Identifier'
|
||||
&& node.property.name === 'propTypes'
|
||||
&& !ast.isAssignmentLHS(node)
|
||||
&& !isAllowedAssignment(node)
|
||||
)) || (
|
||||
// @ts-expect-error The JSXText type is not present in the estree type definitions
|
||||
(node.property.type === 'Literal' || node.property.type === 'JSXText')
|
||||
&& 'value' in node.property
|
||||
&& node.property.value === 'propTypes'
|
||||
&& !ast.isAssignmentLHS(node)
|
||||
&& !isAllowedAssignment(node)
|
||||
)
|
||||
) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: node.property,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
const propTypesNode = node.properties.find((property) => (
|
||||
property.type === 'Property'
|
||||
&& 'name' in property.key
|
||||
&& property.key.name === 'propTypes'
|
||||
));
|
||||
|
||||
if (propTypesNode) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: propTypesNode,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
297
node_modules/eslint-plugin-react/lib/rules/forbid-prop-types.js
generated
vendored
Normal file
297
node_modules/eslint-plugin-react/lib/rules/forbid-prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* @fileoverview Forbid certain propTypes
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const propsUtil = require('../util/props');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = ['any', 'array', 'object'];
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
forbiddenPropType: 'Prop type "{{target}}" is forbidden',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow certain propTypes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('forbid-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
checkContextTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
checkChildContextTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const checkContextTypes = configuration.checkContextTypes || false;
|
||||
const checkChildContextTypes = configuration.checkChildContextTypes || false;
|
||||
let propTypesPackageName = null;
|
||||
let reactPackageName = null;
|
||||
let isForeignPropTypesPackage = false;
|
||||
|
||||
function isPropTypesPackage(node) {
|
||||
return (
|
||||
node.type === 'Identifier'
|
||||
&& (
|
||||
node.name === null
|
||||
|| node.name === propTypesPackageName
|
||||
|| !isForeignPropTypesPackage
|
||||
)
|
||||
) || (
|
||||
node.type === 'MemberExpression'
|
||||
&& (
|
||||
node.object.name === null
|
||||
|| node.object.name === reactPackageName
|
||||
|| !isForeignPropTypesPackage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isForbidden(type) {
|
||||
const forbid = configuration.forbid || DEFAULTS;
|
||||
return forbid.indexOf(type) >= 0;
|
||||
}
|
||||
|
||||
function reportIfForbidden(type, declaration, target) {
|
||||
if (isForbidden(type)) {
|
||||
report(context, messages.forbiddenPropType, 'forbiddenPropType', {
|
||||
node: declaration,
|
||||
data: {
|
||||
target,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shouldCheckContextTypes(node) {
|
||||
if (checkContextTypes && propsUtil.isContextTypesDeclaration(node)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldCheckChildContextTypes(node) {
|
||||
if (checkChildContextTypes && propsUtil.isChildContextTypesDeclaration(node)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if propTypes declarations are forbidden
|
||||
* @param {Array} declarations The array of AST nodes being checked.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkProperties(declarations) {
|
||||
if (declarations) {
|
||||
declarations.forEach((declaration) => {
|
||||
if (declaration.type !== 'Property') {
|
||||
return;
|
||||
}
|
||||
let target;
|
||||
let value = declaration.value;
|
||||
if (
|
||||
value.type === 'MemberExpression'
|
||||
&& value.property
|
||||
&& value.property.name
|
||||
&& value.property.name === 'isRequired'
|
||||
) {
|
||||
value = value.object;
|
||||
}
|
||||
if (astUtil.isCallExpression(value)) {
|
||||
if (!isPropTypesPackage(value.callee)) {
|
||||
return;
|
||||
}
|
||||
value.arguments.forEach((arg) => {
|
||||
const name = arg.type === 'MemberExpression' ? arg.property.name : arg.name;
|
||||
reportIfForbidden(name, declaration, name);
|
||||
});
|
||||
value = value.callee;
|
||||
}
|
||||
if (!isPropTypesPackage(value)) {
|
||||
return;
|
||||
}
|
||||
if (value.property) {
|
||||
target = value.property.name;
|
||||
} else if (value.type === 'Identifier') {
|
||||
target = value.name;
|
||||
}
|
||||
reportIfForbidden(target, declaration, target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ObjectExpression') {
|
||||
checkProperties(node.properties);
|
||||
} else if (node.type === 'Identifier') {
|
||||
const propTypesObject = variableUtil.findVariableByName(context, node, node.name);
|
||||
if (propTypesObject && propTypesObject.properties) {
|
||||
checkProperties(propTypesObject.properties);
|
||||
}
|
||||
} else if (astUtil.isCallExpression(node)) {
|
||||
const innerNode = node.arguments && node.arguments[0];
|
||||
if (
|
||||
propWrapperUtil.isPropWrapperFunction(context, getText(context, node.callee))
|
||||
&& innerNode
|
||||
) {
|
||||
checkNode(innerNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
|
||||
if (node.specifiers.length > 0) {
|
||||
propTypesPackageName = node.specifiers[0].local.name;
|
||||
}
|
||||
} else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
|
||||
if (node.specifiers.length > 0) {
|
||||
reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
|
||||
}
|
||||
if (node.specifiers.length >= 1) {
|
||||
const propTypesSpecifier = node.specifiers.find((specifier) => (
|
||||
'imported' in specifier
|
||||
&& specifier.imported
|
||||
&& specifier.imported.name === 'PropTypes'
|
||||
));
|
||||
if (propTypesSpecifier) {
|
||||
propTypesPackageName = propTypesSpecifier.local.name;
|
||||
}
|
||||
}
|
||||
} else { // package is not imported from "react" or "prop-types"
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (node.specifiers.some((x) => x.local.name === 'PropTypes')) { // assert: node.specifiers.length > 1
|
||||
isForeignPropTypesPackage = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
checkNode(node.value);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode('right' in node.parent && node.parent.right);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression'
|
||||
&& node.callee.object
|
||||
&& !isPropTypesPackage(node.callee.object)
|
||||
&& !propsUtil.isPropTypesDeclaration(node.callee)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
node.arguments.length > 0
|
||||
&& (
|
||||
('name' in node.callee && node.callee.name === 'shape')
|
||||
|| astUtil.getPropertyName(node.callee) === 'shape'
|
||||
)
|
||||
) {
|
||||
checkProperties('properties' in node.arguments[0] && node.arguments[0].properties);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(node)
|
||||
&& !isPropTypesPackage(node)
|
||||
&& !shouldCheckContextTypes(node)
|
||||
&& !shouldCheckChildContextTypes(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnStatement = astUtil.findReturnStatement(node);
|
||||
|
||||
if (returnStatement && returnStatement.argument) {
|
||||
checkNode(returnStatement.argument);
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
node.properties.forEach((property) => {
|
||||
if (!('key' in property) || !property.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!propsUtil.isPropTypesDeclaration(property)
|
||||
&& !isPropTypesPackage(property)
|
||||
&& !shouldCheckContextTypes(property)
|
||||
&& !shouldCheckChildContextTypes(property)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
checkProperties(property.value.properties);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
99
node_modules/eslint-plugin-react/lib/rules/forward-ref-uses-ref.js
generated
vendored
Normal file
99
node_modules/eslint-plugin-react/lib/rules/forward-ref-uses-ref.js
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @fileoverview Require all forwardRef components include a ref parameter
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const isParenthesized = require('../util/ast').isParenthesized;
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getMessageData = require('../util/message');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean} If the node represents the identifier `forwardRef`.
|
||||
*/
|
||||
function isForwardRefIdentifier(node) {
|
||||
return node.type === 'Identifier' && node.name === 'forwardRef';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean} If the node represents a function call `forwardRef()` or `React.forwardRef()`.
|
||||
*/
|
||||
function isForwardRefCall(node) {
|
||||
return (
|
||||
node.type === 'CallExpression'
|
||||
&& (
|
||||
isForwardRefIdentifier(node.callee)
|
||||
|| (node.callee.type === 'MemberExpression' && isForwardRefIdentifier(node.callee.property))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
missingRefParameter: 'forwardRef is used with this component but no ref parameter is set',
|
||||
addRefParameter: 'Add a ref parameter',
|
||||
removeForwardRef: 'Remove forwardRef wrapper',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Require all forwardRef components include a ref parameter',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('forward-ref-uses-ref'),
|
||||
},
|
||||
messages,
|
||||
schema: [],
|
||||
type: 'suggestion',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
return {
|
||||
'FunctionExpression, ArrowFunctionExpression'(node) {
|
||||
if (!isForwardRefCall(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.params.length === 1) {
|
||||
report(context, messages.missingRefParameter, 'missingRefParameter', {
|
||||
node,
|
||||
suggest: [
|
||||
Object.assign(
|
||||
getMessageData('addRefParameter', messages.addRefParameter),
|
||||
{
|
||||
fix(fixer) {
|
||||
const param = node.params[0];
|
||||
// If using shorthand arrow function syntax, add parentheses around the new parameter pair
|
||||
const shouldAddParentheses = node.type === 'ArrowFunctionExpression' && !isParenthesized(context, param);
|
||||
return [].concat(
|
||||
shouldAddParentheses ? fixer.insertTextBefore(param, '(') : [],
|
||||
fixer.insertTextAfter(param, `, ref${shouldAddParentheses ? ')' : ''}`)
|
||||
);
|
||||
},
|
||||
}
|
||||
),
|
||||
Object.assign(
|
||||
getMessageData('removeForwardRef', messages.removeForwardRef),
|
||||
{
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.parent, sourceCode.getText(node));
|
||||
},
|
||||
}
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
285
node_modules/eslint-plugin-react/lib/rules/function-component-definition.js
generated
vendored
Normal file
285
node_modules/eslint-plugin-react/lib/rules/function-component-definition.js
generated
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Standardize the way function component get defined
|
||||
* @author Stefan Wullems
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const arrayIncludes = require('array-includes');
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const reportC = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const propsUtil = require('../util/props');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function buildFunction(template, parts) {
|
||||
return Object.keys(parts).reduce(
|
||||
(acc, key) => acc.replace(`{${key}}`, () => parts[key] || ''),
|
||||
template
|
||||
);
|
||||
}
|
||||
|
||||
const NAMED_FUNCTION_TEMPLATES = {
|
||||
'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}',
|
||||
'arrow-function': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
|
||||
'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
|
||||
};
|
||||
|
||||
const UNNAMED_FUNCTION_TEMPLATES = {
|
||||
'function-expression': 'function{typeParams}({params}){returnType} {body}',
|
||||
'arrow-function': '{typeParams}({params}){returnType} => {body}',
|
||||
};
|
||||
|
||||
function hasOneUnconstrainedTypeParam(node) {
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
|
||||
return nodeTypeArguments
|
||||
&& nodeTypeArguments.params
|
||||
&& nodeTypeArguments.params.length === 1
|
||||
&& !nodeTypeArguments.params[0].constraint;
|
||||
}
|
||||
|
||||
function hasName(node) {
|
||||
return (
|
||||
node.type === 'FunctionDeclaration'
|
||||
|| node.parent.type === 'VariableDeclarator'
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeText(prop, source) {
|
||||
if (!prop) return null;
|
||||
return source.slice(prop.range[0], prop.range[1]);
|
||||
}
|
||||
|
||||
function getName(node) {
|
||||
if (node.type === 'FunctionDeclaration') {
|
||||
return node.id.name;
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
|| node.type === 'FunctionExpression'
|
||||
) {
|
||||
return hasName(node) && node.parent.id.name;
|
||||
}
|
||||
}
|
||||
|
||||
function getParams(node, source) {
|
||||
if (node.params.length === 0) return null;
|
||||
return source.slice(
|
||||
node.params[0].range[0],
|
||||
node.params[node.params.length - 1].range[1]
|
||||
);
|
||||
}
|
||||
|
||||
function getBody(node, source) {
|
||||
const range = node.body.range;
|
||||
|
||||
if (node.body.type !== 'BlockStatement') {
|
||||
return ['{', ` return ${source.slice(range[0], range[1])}`, '}'].join('\n');
|
||||
}
|
||||
|
||||
return source.slice(range[0], range[1]);
|
||||
}
|
||||
|
||||
function getTypeAnnotation(node, source) {
|
||||
if (!hasName(node) || node.type === 'FunctionDeclaration') return;
|
||||
|
||||
if (
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
|| node.type === 'FunctionExpression'
|
||||
) {
|
||||
return getNodeText(node.parent.id.typeAnnotation, source);
|
||||
}
|
||||
}
|
||||
|
||||
function isUnfixableBecauseOfExport(node) {
|
||||
return (
|
||||
node.type === 'FunctionDeclaration'
|
||||
&& node.parent
|
||||
&& node.parent.type === 'ExportDefaultDeclaration'
|
||||
);
|
||||
}
|
||||
|
||||
function isFunctionExpressionWithName(node) {
|
||||
return node.type === 'FunctionExpression' && node.id && node.id.name;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
'function-declaration': 'Function component is not a function declaration',
|
||||
'function-expression': 'Function component is not a function expression',
|
||||
'arrow-function': 'Function component is not an arrow function',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce a specific function type for function components',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('function-component-definition'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
namedComponents: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: [
|
||||
'function-declaration',
|
||||
'arrow-function',
|
||||
'function-expression',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'function-declaration',
|
||||
'arrow-function',
|
||||
'function-expression',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
unnamedComponents: {
|
||||
anyOf: [
|
||||
{ enum: ['arrow-function', 'function-expression'] },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: ['arrow-function', 'function-expression'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
let fileVarType = 'var';
|
||||
|
||||
const namedConfig = [].concat(
|
||||
configuration.namedComponents || 'function-declaration'
|
||||
);
|
||||
const unnamedConfig = [].concat(
|
||||
configuration.unnamedComponents || 'function-expression'
|
||||
);
|
||||
|
||||
function getFixer(node, options) {
|
||||
const source = getText(context);
|
||||
|
||||
const typeAnnotation = getTypeAnnotation(node, source);
|
||||
|
||||
if (options.type === 'function-declaration' && typeAnnotation) {
|
||||
return;
|
||||
}
|
||||
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) {
|
||||
return;
|
||||
}
|
||||
if (isUnfixableBecauseOfExport(node)) return;
|
||||
if (isFunctionExpressionWithName(node)) return;
|
||||
let varType = fileVarType;
|
||||
if (
|
||||
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')
|
||||
&& node.parent.type === 'VariableDeclarator'
|
||||
) {
|
||||
varType = node.parent.parent.kind;
|
||||
}
|
||||
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
return (fixer) => fixer.replaceTextRange(
|
||||
options.range,
|
||||
buildFunction(options.template, {
|
||||
typeAnnotation,
|
||||
typeParams: getNodeText(nodeTypeArguments, source),
|
||||
params: getParams(node, source),
|
||||
returnType: getNodeText(node.returnType, source),
|
||||
body: getBody(node, source),
|
||||
name: getName(node),
|
||||
varType,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function report(node, options) {
|
||||
reportC(context, messages[options.messageId], options.messageId, {
|
||||
node,
|
||||
fix: getFixer(node, options.fixerOptions),
|
||||
});
|
||||
}
|
||||
|
||||
function validate(node, functionType) {
|
||||
if (!components.get(node)) return;
|
||||
|
||||
if (node.parent && node.parent.type === 'Property') return;
|
||||
|
||||
if (hasName(node) && !arrayIncludes(namedConfig, functionType)) {
|
||||
report(node, {
|
||||
messageId: namedConfig[0],
|
||||
fixerOptions: {
|
||||
type: namedConfig[0],
|
||||
template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]],
|
||||
range:
|
||||
node.type === 'FunctionDeclaration'
|
||||
? node.range
|
||||
: node.parent.parent.range,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!hasName(node) && !arrayIncludes(unnamedConfig, functionType)) {
|
||||
report(node, {
|
||||
messageId: unnamedConfig[0],
|
||||
fixerOptions: {
|
||||
type: unnamedConfig[0],
|
||||
template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig[0]],
|
||||
range: node.range,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
const validatePairs = [];
|
||||
let hasES6OrJsx = false;
|
||||
return {
|
||||
FunctionDeclaration(node) {
|
||||
validatePairs.push([node, 'function-declaration']);
|
||||
},
|
||||
ArrowFunctionExpression(node) {
|
||||
validatePairs.push([node, 'arrow-function']);
|
||||
},
|
||||
FunctionExpression(node) {
|
||||
validatePairs.push([node, 'function-expression']);
|
||||
},
|
||||
VariableDeclaration(node) {
|
||||
hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let';
|
||||
},
|
||||
'Program:exit'() {
|
||||
if (hasES6OrJsx) fileVarType = 'const';
|
||||
validatePairs.forEach((pair) => validate(pair[0], pair[1]));
|
||||
},
|
||||
'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() {
|
||||
hasES6OrJsx = true;
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
205
node_modules/eslint-plugin-react/lib/rules/hook-use-state.js
generated
vendored
Normal file
205
node_modules/eslint-plugin-react/lib/rules/hook-use-state.js
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @fileoverview Ensure symmetric naming of useState hook value and setter variables
|
||||
* @author Duncan Beevers
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getMessageData = require('../util/message');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function isNodeDestructuring(node) {
|
||||
return node && (node.type === 'ArrayPattern' || node.type === 'ObjectPattern');
|
||||
}
|
||||
|
||||
const messages = {
|
||||
useStateErrorMessage: 'useState call is not destructured into value + setter pair',
|
||||
useStateErrorMessageOrAddOption: 'useState call is not destructured into value + setter pair (you can allow destructuring by enabling "allowDestructuredState" option)',
|
||||
suggestPair: 'Destructure useState call into value + setter pair',
|
||||
suggestMemo: 'Replace useState call with useMemo',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Ensure destructuring and symmetric naming of useState hook value and setter variables',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('hook-use-state'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowDestructuredState: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
type: 'suggestion',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, util) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowDestructuredState = configuration.allowDestructuredState || false;
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const isImmediateReturn = node.parent
|
||||
&& node.parent.type === 'ReturnStatement';
|
||||
|
||||
if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDestructuringDeclarator = node.parent
|
||||
&& node.parent.type === 'VariableDeclarator'
|
||||
&& node.parent.id.type === 'ArrayPattern';
|
||||
|
||||
if (!isDestructuringDeclarator) {
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessage,
|
||||
'useStateErrorMessage',
|
||||
{
|
||||
node,
|
||||
suggest: false,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const variableNodes = node.parent.id.elements;
|
||||
const valueVariable = variableNodes[0];
|
||||
const setterVariable = variableNodes[1];
|
||||
const isOnlyValueDestructuring = isNodeDestructuring(valueVariable) && !isNodeDestructuring(setterVariable);
|
||||
|
||||
if (allowDestructuredState && isOnlyValueDestructuring) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueVariableName = valueVariable
|
||||
? valueVariable.name
|
||||
: undefined;
|
||||
|
||||
const setterVariableName = setterVariable
|
||||
? setterVariable.name
|
||||
: undefined;
|
||||
|
||||
const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined;
|
||||
const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined;
|
||||
const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined;
|
||||
const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
|
||||
`set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`,
|
||||
`set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
|
||||
] : [];
|
||||
|
||||
const isSymmetricGetterSetterPair = valueVariable
|
||||
&& setterVariable
|
||||
&& expectedSetterVariableNames.indexOf(setterVariableName) !== -1
|
||||
&& variableNodes.length === 2;
|
||||
|
||||
if (!isSymmetricGetterSetterPair) {
|
||||
const suggestions = [
|
||||
Object.assign(
|
||||
getMessageData('suggestPair', messages.suggestPair),
|
||||
{
|
||||
fix(fixer) {
|
||||
if (expectedSetterVariableNames.length > 0) {
|
||||
return fixer.replaceTextRange(
|
||||
node.parent.id.range,
|
||||
`[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
const defaultReactImports = components.getDefaultReactImports();
|
||||
const defaultReactImportSpecifier = defaultReactImports
|
||||
? defaultReactImports[0]
|
||||
: undefined;
|
||||
|
||||
const defaultReactImportName = defaultReactImportSpecifier
|
||||
? defaultReactImportSpecifier.local.name
|
||||
: undefined;
|
||||
|
||||
const namedReactImports = components.getNamedReactImports();
|
||||
const useStateReactImportSpecifier = namedReactImports
|
||||
? namedReactImports.find((specifier) => specifier.imported.name === 'useState')
|
||||
: undefined;
|
||||
|
||||
const isSingleGetter = valueVariable && variableNodes.length === 1;
|
||||
const isUseStateCalledWithSingleArgument = node.arguments.length === 1;
|
||||
if (isSingleGetter && isUseStateCalledWithSingleArgument) {
|
||||
const useMemoReactImportSpecifier = namedReactImports
|
||||
&& namedReactImports.find((specifier) => specifier.imported.name === 'useMemo');
|
||||
|
||||
let useMemoCode;
|
||||
if (useMemoReactImportSpecifier) {
|
||||
useMemoCode = useMemoReactImportSpecifier.local.name;
|
||||
} else if (defaultReactImportName) {
|
||||
useMemoCode = `${defaultReactImportName}.useMemo`;
|
||||
} else {
|
||||
useMemoCode = 'useMemo';
|
||||
}
|
||||
|
||||
suggestions.unshift(Object.assign(
|
||||
getMessageData('suggestMemo', messages.suggestMemo),
|
||||
{
|
||||
fix: (fixer) => [
|
||||
// Add useMemo import, if necessary
|
||||
useStateReactImportSpecifier
|
||||
&& (!useMemoReactImportSpecifier || defaultReactImportName)
|
||||
&& fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'),
|
||||
// Convert single-value destructure to simple assignment
|
||||
fixer.replaceTextRange(node.parent.id.range, valueVariableName),
|
||||
// Convert useState call to useMemo + arrow function + dependency array
|
||||
fixer.replaceTextRange(
|
||||
node.range,
|
||||
`${useMemoCode}(() => ${getText(context, node.arguments[0])}, [])`
|
||||
),
|
||||
].filter(Boolean),
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
if (isOnlyValueDestructuring) {
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessageOrAddOption,
|
||||
'useStateErrorMessageOrAddOption',
|
||||
{
|
||||
node: node.parent.id,
|
||||
suggest: false,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
report(
|
||||
context,
|
||||
messages.useStateErrorMessage,
|
||||
'useStateErrorMessage',
|
||||
{
|
||||
node: node.parent.id,
|
||||
suggest: suggestions,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
143
node_modules/eslint-plugin-react/lib/rules/iframe-missing-sandbox.js
generated
vendored
Normal file
143
node_modules/eslint-plugin-react/lib/rules/iframe-missing-sandbox.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @fileoverview TBD
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
const messages = {
|
||||
attributeMissing: 'An iframe element is missing a sandbox attribute',
|
||||
invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"',
|
||||
invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid',
|
||||
};
|
||||
|
||||
const ALLOWED_VALUES = [
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
|
||||
'',
|
||||
'allow-downloads-without-user-activation',
|
||||
'allow-downloads',
|
||||
'allow-forms',
|
||||
'allow-modals',
|
||||
'allow-orientation-lock',
|
||||
'allow-pointer-lock',
|
||||
'allow-popups',
|
||||
'allow-popups-to-escape-sandbox',
|
||||
'allow-presentation',
|
||||
'allow-same-origin',
|
||||
'allow-scripts',
|
||||
'allow-storage-access-by-user-activation',
|
||||
'allow-top-navigation',
|
||||
'allow-top-navigation-by-user-activation',
|
||||
];
|
||||
|
||||
function validateSandboxAttribute(context, node, attribute) {
|
||||
if (typeof attribute !== 'string') {
|
||||
// Only string literals are supported for now
|
||||
return;
|
||||
}
|
||||
const values = attribute.split(' ');
|
||||
let allowScripts = false;
|
||||
let allowSameOrigin = false;
|
||||
values.forEach((attributeValue) => {
|
||||
const trimmedAttributeValue = attributeValue.trim();
|
||||
if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) {
|
||||
report(context, messages.invalidValue, 'invalidValue', {
|
||||
node,
|
||||
data: {
|
||||
value: trimmedAttributeValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (trimmedAttributeValue === 'allow-scripts') {
|
||||
allowScripts = true;
|
||||
}
|
||||
if (trimmedAttributeValue === 'allow-same-origin') {
|
||||
allowSameOrigin = true;
|
||||
}
|
||||
});
|
||||
if (allowScripts && allowSameOrigin) {
|
||||
report(context, messages.invalidCombination, 'invalidCombination', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkAttributes(context, node) {
|
||||
let sandboxAttributeFound = false;
|
||||
node.attributes.forEach((attribute) => {
|
||||
if (attribute.type === 'JSXAttribute'
|
||||
&& attribute.name
|
||||
&& attribute.name.type === 'JSXIdentifier'
|
||||
&& attribute.name.name === 'sandbox'
|
||||
) {
|
||||
sandboxAttributeFound = true;
|
||||
if (
|
||||
attribute.value
|
||||
&& attribute.value.type === 'Literal'
|
||||
&& attribute.value.value
|
||||
) {
|
||||
validateSandboxAttribute(context, node, attribute.value.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!sandboxAttributeFound) {
|
||||
report(context, messages.attributeMissing, 'attributeMissing', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkProps(context, node) {
|
||||
let sandboxAttributeFound = false;
|
||||
if (node.arguments.length > 1) {
|
||||
const props = node.arguments[1];
|
||||
const sandboxProp = props.properties && props.properties.find((x) => x.type === 'Property' && x.key.name === 'sandbox');
|
||||
if (sandboxProp) {
|
||||
sandboxAttributeFound = true;
|
||||
if (sandboxProp.value && sandboxProp.value.type === 'Literal' && sandboxProp.value.value) {
|
||||
validateSandboxAttribute(context, node, sandboxProp.value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sandboxAttributeFound) {
|
||||
report(context, messages.attributeMissing, 'attributeMissing', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce sandbox attribute on iframe elements',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('iframe-missing-sandbox'),
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
'JSXOpeningElement[name.name="iframe"]'(node) {
|
||||
checkAttributes(context, node);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (isCreateElement(context, node) && node.arguments && node.arguments.length > 0) {
|
||||
const tag = node.arguments[0];
|
||||
if (tag.type === 'Literal' && tag.value === 'iframe') {
|
||||
checkProps(context, node);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
110
node_modules/eslint-plugin-react/lib/rules/index.js
generated
vendored
Normal file
110
node_modules/eslint-plugin-react/lib/rules/index.js
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint global-require: 0 */
|
||||
|
||||
/** @satisfies {Record<string, import('eslint').Rule.RuleModule>} */
|
||||
module.exports = {
|
||||
'boolean-prop-naming': require('./boolean-prop-naming'),
|
||||
'button-has-type': require('./button-has-type'),
|
||||
'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'),
|
||||
'default-props-match-prop-types': require('./default-props-match-prop-types'),
|
||||
'destructuring-assignment': require('./destructuring-assignment'),
|
||||
'display-name': require('./display-name'),
|
||||
'forbid-component-props': require('./forbid-component-props'),
|
||||
'forbid-dom-props': require('./forbid-dom-props'),
|
||||
'forbid-elements': require('./forbid-elements'),
|
||||
'forbid-foreign-prop-types': require('./forbid-foreign-prop-types'),
|
||||
'forbid-prop-types': require('./forbid-prop-types'),
|
||||
'forward-ref-uses-ref': require('./forward-ref-uses-ref'),
|
||||
'function-component-definition': require('./function-component-definition'),
|
||||
'hook-use-state': require('./hook-use-state'),
|
||||
'iframe-missing-sandbox': require('./iframe-missing-sandbox'),
|
||||
'jsx-boolean-value': require('./jsx-boolean-value'),
|
||||
'jsx-child-element-spacing': require('./jsx-child-element-spacing'),
|
||||
'jsx-closing-bracket-location': require('./jsx-closing-bracket-location'),
|
||||
'jsx-closing-tag-location': require('./jsx-closing-tag-location'),
|
||||
'jsx-curly-spacing': require('./jsx-curly-spacing'),
|
||||
'jsx-curly-newline': require('./jsx-curly-newline'),
|
||||
'jsx-equals-spacing': require('./jsx-equals-spacing'),
|
||||
'jsx-filename-extension': require('./jsx-filename-extension'),
|
||||
'jsx-first-prop-new-line': require('./jsx-first-prop-new-line'),
|
||||
'jsx-handler-names': require('./jsx-handler-names'),
|
||||
'jsx-indent': require('./jsx-indent'),
|
||||
'jsx-indent-props': require('./jsx-indent-props'),
|
||||
'jsx-key': require('./jsx-key'),
|
||||
'jsx-max-depth': require('./jsx-max-depth'),
|
||||
'jsx-max-props-per-line': require('./jsx-max-props-per-line'),
|
||||
'jsx-newline': require('./jsx-newline'),
|
||||
'jsx-no-bind': require('./jsx-no-bind'),
|
||||
'jsx-no-comment-textnodes': require('./jsx-no-comment-textnodes'),
|
||||
'jsx-no-constructed-context-values': require('./jsx-no-constructed-context-values'),
|
||||
'jsx-no-duplicate-props': require('./jsx-no-duplicate-props'),
|
||||
'jsx-no-leaked-render': require('./jsx-no-leaked-render'),
|
||||
'jsx-no-literals': require('./jsx-no-literals'),
|
||||
'jsx-no-script-url': require('./jsx-no-script-url'),
|
||||
'jsx-no-target-blank': require('./jsx-no-target-blank'),
|
||||
'jsx-no-useless-fragment': require('./jsx-no-useless-fragment'),
|
||||
'jsx-one-expression-per-line': require('./jsx-one-expression-per-line'),
|
||||
'jsx-no-undef': require('./jsx-no-undef'),
|
||||
'jsx-curly-brace-presence': require('./jsx-curly-brace-presence'),
|
||||
'jsx-pascal-case': require('./jsx-pascal-case'),
|
||||
'jsx-fragments': require('./jsx-fragments'),
|
||||
'jsx-props-no-multi-spaces': require('./jsx-props-no-multi-spaces'),
|
||||
'jsx-props-no-spreading': require('./jsx-props-no-spreading'),
|
||||
'jsx-props-no-spread-multi': require('./jsx-props-no-spread-multi'),
|
||||
'jsx-sort-default-props': require('./jsx-sort-default-props'),
|
||||
'jsx-sort-props': require('./jsx-sort-props'),
|
||||
'jsx-space-before-closing': require('./jsx-space-before-closing'),
|
||||
'jsx-tag-spacing': require('./jsx-tag-spacing'),
|
||||
'jsx-uses-react': require('./jsx-uses-react'),
|
||||
'jsx-uses-vars': require('./jsx-uses-vars'),
|
||||
'jsx-wrap-multilines': require('./jsx-wrap-multilines'),
|
||||
'no-invalid-html-attribute': require('./no-invalid-html-attribute'),
|
||||
'no-access-state-in-setstate': require('./no-access-state-in-setstate'),
|
||||
'no-adjacent-inline-elements': require('./no-adjacent-inline-elements'),
|
||||
'no-array-index-key': require('./no-array-index-key'),
|
||||
'no-arrow-function-lifecycle': require('./no-arrow-function-lifecycle'),
|
||||
'no-children-prop': require('./no-children-prop'),
|
||||
'no-danger': require('./no-danger'),
|
||||
'no-danger-with-children': require('./no-danger-with-children'),
|
||||
'no-deprecated': require('./no-deprecated'),
|
||||
'no-did-mount-set-state': require('./no-did-mount-set-state'),
|
||||
'no-did-update-set-state': require('./no-did-update-set-state'),
|
||||
'no-direct-mutation-state': require('./no-direct-mutation-state'),
|
||||
'no-find-dom-node': require('./no-find-dom-node'),
|
||||
'no-is-mounted': require('./no-is-mounted'),
|
||||
'no-multi-comp': require('./no-multi-comp'),
|
||||
'no-namespace': require('./no-namespace'),
|
||||
'no-set-state': require('./no-set-state'),
|
||||
'no-string-refs': require('./no-string-refs'),
|
||||
'no-redundant-should-component-update': require('./no-redundant-should-component-update'),
|
||||
'no-render-return-value': require('./no-render-return-value'),
|
||||
'no-this-in-sfc': require('./no-this-in-sfc'),
|
||||
'no-typos': require('./no-typos'),
|
||||
'no-unescaped-entities': require('./no-unescaped-entities'),
|
||||
'no-unknown-property': require('./no-unknown-property'),
|
||||
'no-unsafe': require('./no-unsafe'),
|
||||
'no-unstable-nested-components': require('./no-unstable-nested-components'),
|
||||
'no-unused-class-component-methods': require('./no-unused-class-component-methods'),
|
||||
'no-unused-prop-types': require('./no-unused-prop-types'),
|
||||
'no-unused-state': require('./no-unused-state'),
|
||||
'no-object-type-as-default-prop': require('./no-object-type-as-default-prop'),
|
||||
'no-will-update-set-state': require('./no-will-update-set-state'),
|
||||
'prefer-es6-class': require('./prefer-es6-class'),
|
||||
'prefer-exact-props': require('./prefer-exact-props'),
|
||||
'prefer-read-only-props': require('./prefer-read-only-props'),
|
||||
'prefer-stateless-function': require('./prefer-stateless-function'),
|
||||
'prop-types': require('./prop-types'),
|
||||
'react-in-jsx-scope': require('./react-in-jsx-scope'),
|
||||
'require-default-props': require('./require-default-props'),
|
||||
'require-optimization': require('./require-optimization'),
|
||||
'require-render-return': require('./require-render-return'),
|
||||
'self-closing-comp': require('./self-closing-comp'),
|
||||
'sort-comp': require('./sort-comp'),
|
||||
'sort-default-props': require('./sort-default-props'),
|
||||
'sort-prop-types': require('./sort-prop-types'),
|
||||
'state-in-constructor': require('./state-in-constructor'),
|
||||
'static-property-placement': require('./static-property-placement'),
|
||||
'style-prop-object': require('./style-prop-object'),
|
||||
'void-dom-elements-no-children': require('./void-dom-elements-no-children'),
|
||||
};
|
||||
169
node_modules/eslint-plugin-react/lib/rules/jsx-boolean-value.js
generated
vendored
Normal file
169
node_modules/eslint-plugin-react/lib/rules/jsx-boolean-value.js
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @fileoverview Enforce boolean attributes notation in JSX
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const exceptionsSchema = {
|
||||
type: 'array',
|
||||
items: { type: 'string', minLength: 1 },
|
||||
uniqueItems: true,
|
||||
};
|
||||
|
||||
const ALWAYS = 'always';
|
||||
const NEVER = 'never';
|
||||
|
||||
/**
|
||||
* @param {string} configuration
|
||||
* @param {Set<string>} exceptions
|
||||
* @param {string} propName
|
||||
* @returns {boolean} propName
|
||||
*/
|
||||
function isAlways(configuration, exceptions, propName) {
|
||||
const isException = exceptions.has(propName);
|
||||
if (configuration === ALWAYS) {
|
||||
return !isException;
|
||||
}
|
||||
return isException;
|
||||
}
|
||||
/**
|
||||
* @param {string} configuration
|
||||
* @param {Set<string>} exceptions
|
||||
* @param {string} propName
|
||||
* @returns {boolean} propName
|
||||
*/
|
||||
function isNever(configuration, exceptions, propName) {
|
||||
const isException = exceptions.has(propName);
|
||||
if (configuration === NEVER) {
|
||||
return !isException;
|
||||
}
|
||||
return isException;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
omitBoolean: 'Value must be omitted for boolean attribute `{{propName}}`',
|
||||
setBoolean: 'Value must be set for boolean attribute `{{propName}}`',
|
||||
omitPropAndBoolean: 'Value must be omitted for `false` attribute: `{{propName}}`',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce boolean attributes notation in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-boolean-value'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: {
|
||||
anyOf: [{
|
||||
type: 'array',
|
||||
items: [{ enum: [ALWAYS, NEVER] }],
|
||||
additionalItems: false,
|
||||
}, {
|
||||
type: 'array',
|
||||
items: [{
|
||||
enum: [ALWAYS],
|
||||
}, {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
[NEVER]: exceptionsSchema,
|
||||
assumeUndefinedIsFalse: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
additionalItems: false,
|
||||
}, {
|
||||
type: 'array',
|
||||
items: [{
|
||||
enum: [NEVER],
|
||||
}, {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
[ALWAYS]: exceptionsSchema,
|
||||
assumeUndefinedIsFalse: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
additionalItems: false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || NEVER;
|
||||
const configObject = context.options[1] || {};
|
||||
const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []);
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const propName = node.name && node.name.name;
|
||||
const value = node.value;
|
||||
|
||||
if (
|
||||
isAlways(configuration, exceptions, propName)
|
||||
&& value === null
|
||||
) {
|
||||
const messageId = 'setBoolean';
|
||||
const data = { propName };
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
data,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(node, '={true}');
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
isNever(configuration, exceptions, propName)
|
||||
&& value
|
||||
&& value.type === 'JSXExpressionContainer'
|
||||
&& value.expression.value === true
|
||||
) {
|
||||
const messageId = 'omitBoolean';
|
||||
const data = { propName };
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
data,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([node.name.range[1], value.range[1]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
isNever(configuration, exceptions, propName)
|
||||
&& configObject.assumeUndefinedIsFalse
|
||||
&& value
|
||||
&& value.type === 'JSXExpressionContainer'
|
||||
&& value.expression.value === false
|
||||
) {
|
||||
const messageId = 'omitPropAndBoolean';
|
||||
const data = { propName };
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
data,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([node.name.range[0], value.range[1]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
117
node_modules/eslint-plugin-react/lib/rules/jsx-child-element-spacing.js
generated
vendored
Normal file
117
node_modules/eslint-plugin-react/lib/rules/jsx-child-element-spacing.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
||||
|
||||
// Note: 'br' is not included because whitespace around br tags is inconsequential to the rendered output
|
||||
const INLINE_ELEMENTS = new Set([
|
||||
'a',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'b',
|
||||
'bdo',
|
||||
'big',
|
||||
'button',
|
||||
'cite',
|
||||
'code',
|
||||
'dfn',
|
||||
'em',
|
||||
'i',
|
||||
'img',
|
||||
'input',
|
||||
'kbd',
|
||||
'label',
|
||||
'map',
|
||||
'object',
|
||||
'q',
|
||||
'samp',
|
||||
'script',
|
||||
'select',
|
||||
'small',
|
||||
'span',
|
||||
'strong',
|
||||
'sub',
|
||||
'sup',
|
||||
'textarea',
|
||||
'tt',
|
||||
'var',
|
||||
]);
|
||||
|
||||
const messages = {
|
||||
spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}',
|
||||
spacingBeforeNext: 'Ambiguous spacing before next element {{element}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-child-element-spacing'),
|
||||
},
|
||||
fixable: null,
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
|
||||
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
|
||||
|
||||
const elementName = (node) => (
|
||||
node.openingElement
|
||||
&& node.openingElement.name
|
||||
&& node.openingElement.name.type === 'JSXIdentifier'
|
||||
&& node.openingElement.name.name
|
||||
);
|
||||
|
||||
const isInlineElement = (node) => (
|
||||
node.type === 'JSXElement'
|
||||
&& INLINE_ELEMENTS.has(elementName(node))
|
||||
);
|
||||
|
||||
const handleJSX = (node) => {
|
||||
let lastChild = null;
|
||||
let child = null;
|
||||
(node.children.concat([null])).forEach((nextChild) => {
|
||||
if (
|
||||
(lastChild || nextChild)
|
||||
&& (!lastChild || isInlineElement(lastChild))
|
||||
&& (child && (child.type === 'Literal' || child.type === 'JSXText'))
|
||||
&& (!nextChild || isInlineElement(nextChild))
|
||||
&& true
|
||||
) {
|
||||
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
|
||||
report(context, messages.spacingAfterPrev, 'spacingAfterPrev', {
|
||||
node: lastChild,
|
||||
loc: lastChild.loc.end,
|
||||
data: {
|
||||
element: elementName(lastChild),
|
||||
},
|
||||
});
|
||||
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
|
||||
report(context, messages.spacingBeforeNext, 'spacingBeforeNext', {
|
||||
node: nextChild,
|
||||
loc: nextChild.loc.start,
|
||||
data: {
|
||||
element: elementName(nextChild),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
lastChild = child;
|
||||
child = nextChild;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX,
|
||||
};
|
||||
},
|
||||
};
|
||||
315
node_modules/eslint-plugin-react/lib/rules/jsx-closing-bracket-location.js
generated
vendored
Normal file
315
node_modules/eslint-plugin-react/lib/rules/jsx-closing-bracket-location.js
generated
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @fileoverview Validate closing bracket location in JSX
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const repeat = require('string.prototype.repeat');
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
bracketLocation: 'The closing bracket must be {{location}}{{details}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce closing bracket location in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-closing-bracket-location'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
anyOf: [
|
||||
{
|
||||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
nonEmpty: {
|
||||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
|
||||
},
|
||||
selfClosing: {
|
||||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const MESSAGE_LOCATION = {
|
||||
'after-props': 'placed after the last prop',
|
||||
'after-tag': 'placed after the opening tag',
|
||||
'props-aligned': 'aligned with the last prop',
|
||||
'tag-aligned': 'aligned with the opening tag',
|
||||
'line-aligned': 'aligned with the line containing the opening tag',
|
||||
};
|
||||
const DEFAULT_LOCATION = 'tag-aligned';
|
||||
|
||||
const config = context.options[0];
|
||||
const options = {
|
||||
nonEmpty: DEFAULT_LOCATION,
|
||||
selfClosing: DEFAULT_LOCATION,
|
||||
};
|
||||
|
||||
if (typeof config === 'string') {
|
||||
// simple shorthand [1, 'something']
|
||||
options.nonEmpty = config;
|
||||
options.selfClosing = config;
|
||||
} else if (typeof config === 'object') {
|
||||
// [1, {location: 'something'}] (back-compat)
|
||||
if (has(config, 'location')) {
|
||||
options.nonEmpty = config.location;
|
||||
options.selfClosing = config.location;
|
||||
}
|
||||
// [1, {nonEmpty: 'something'}]
|
||||
if (has(config, 'nonEmpty')) {
|
||||
options.nonEmpty = config.nonEmpty;
|
||||
}
|
||||
// [1, {selfClosing: 'something'}]
|
||||
if (has(config, 'selfClosing')) {
|
||||
options.selfClosing = config.selfClosing;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expected location for the closing bracket
|
||||
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
|
||||
* @return {string} Expected location for the closing bracket
|
||||
*/
|
||||
function getExpectedLocation(tokens) {
|
||||
let location;
|
||||
// Is always after the opening tag if there is no props
|
||||
if (typeof tokens.lastProp === 'undefined') {
|
||||
location = 'after-tag';
|
||||
// Is always after the last prop if this one is on the same line as the opening bracket
|
||||
} else if (tokens.opening.line === tokens.lastProp.lastLine) {
|
||||
location = 'after-props';
|
||||
// Else use configuration dependent on selfClosing property
|
||||
} else {
|
||||
location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct 0-indexed column for the closing bracket, given the
|
||||
* expected location.
|
||||
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
|
||||
* @param {string} expectedLocation Expected location for the closing bracket
|
||||
* @return {?Number} The correct column for the closing bracket, or null
|
||||
*/
|
||||
function getCorrectColumn(tokens, expectedLocation) {
|
||||
switch (expectedLocation) {
|
||||
case 'props-aligned':
|
||||
return tokens.lastProp.column;
|
||||
case 'tag-aligned':
|
||||
return tokens.opening.column;
|
||||
case 'line-aligned':
|
||||
return tokens.openingStartOfLine.column;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the closing bracket is correctly located
|
||||
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
|
||||
* @param {string} expectedLocation Expected location for the closing bracket
|
||||
* @return {boolean} True if the closing bracket is correctly located, false if not
|
||||
*/
|
||||
function hasCorrectLocation(tokens, expectedLocation) {
|
||||
switch (expectedLocation) {
|
||||
case 'after-tag':
|
||||
return tokens.tag.line === tokens.closing.line;
|
||||
case 'after-props':
|
||||
return tokens.lastProp.lastLine === tokens.closing.line;
|
||||
case 'props-aligned':
|
||||
case 'tag-aligned':
|
||||
case 'line-aligned': {
|
||||
const correctColumn = getCorrectColumn(tokens, expectedLocation);
|
||||
return correctColumn === tokens.closing.column;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the characters used for indentation on the line to be matched
|
||||
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
|
||||
* @param {string} expectedLocation Expected location for the closing bracket
|
||||
* @param {number} [correctColumn] Expected column for the closing bracket. Default to 0
|
||||
* @return {string} The characters used for indentation
|
||||
*/
|
||||
function getIndentation(tokens, expectedLocation, correctColumn) {
|
||||
const newColumn = correctColumn || 0;
|
||||
let indentation;
|
||||
let spaces = '';
|
||||
switch (expectedLocation) {
|
||||
case 'props-aligned':
|
||||
indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.lastProp.firstLine - 1])[0];
|
||||
break;
|
||||
case 'tag-aligned':
|
||||
case 'line-aligned':
|
||||
indentation = /^\s*/.exec(getSourceCode(context).lines[tokens.opening.line - 1])[0];
|
||||
break;
|
||||
default:
|
||||
indentation = '';
|
||||
}
|
||||
if (indentation.length + 1 < newColumn) {
|
||||
// Non-whitespace characters were included in the column offset
|
||||
spaces = repeat(' ', +correctColumn - indentation.length);
|
||||
}
|
||||
return indentation + spaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locations of the opening bracket, closing bracket, last prop, and
|
||||
* start of opening line.
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {Object} Locations of the opening bracket, closing bracket, last
|
||||
* prop and start of opening line.
|
||||
*/
|
||||
function getTokensLocations(node) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const opening = sourceCode.getFirstToken(node).loc.start;
|
||||
const closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
|
||||
const tag = sourceCode.getFirstToken(node.name).loc.start;
|
||||
let lastProp;
|
||||
if (node.attributes.length) {
|
||||
lastProp = node.attributes[node.attributes.length - 1];
|
||||
lastProp = {
|
||||
column: sourceCode.getFirstToken(lastProp).loc.start.column,
|
||||
firstLine: sourceCode.getFirstToken(lastProp).loc.start.line,
|
||||
lastLine: sourceCode.getLastToken(lastProp).loc.end.line,
|
||||
};
|
||||
}
|
||||
const openingLine = sourceCode.lines[opening.line - 1];
|
||||
const closingLine = sourceCode.lines[closing.line - 1];
|
||||
const isTab = {
|
||||
openTab: /^\t/.test(openingLine),
|
||||
closeTab: /^\t/.test(closingLine),
|
||||
};
|
||||
const openingStartOfLine = {
|
||||
column: /^\s*/.exec(openingLine)[0].length,
|
||||
line: opening.line,
|
||||
};
|
||||
return {
|
||||
isTab,
|
||||
tag,
|
||||
opening,
|
||||
closing,
|
||||
lastProp,
|
||||
selfClosing: node.selfClosing,
|
||||
openingStartOfLine,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an unique ID for a given JSXOpeningElement
|
||||
*
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {string} Unique ID (based on its range)
|
||||
*/
|
||||
function getOpeningElementId(node) {
|
||||
return node.range.join(':');
|
||||
}
|
||||
|
||||
const lastAttributeNode = {};
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
lastAttributeNode[getOpeningElementId(node.parent)] = node;
|
||||
},
|
||||
|
||||
JSXSpreadAttribute(node) {
|
||||
lastAttributeNode[getOpeningElementId(node.parent)] = node;
|
||||
},
|
||||
|
||||
'JSXOpeningElement:exit'(node) {
|
||||
const attributeNode = lastAttributeNode[getOpeningElementId(node)];
|
||||
const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null;
|
||||
|
||||
let expectedNextLine;
|
||||
const tokens = getTokensLocations(node);
|
||||
const expectedLocation = getExpectedLocation(tokens);
|
||||
let usingSameIndentation = true;
|
||||
|
||||
if (expectedLocation === 'tag-aligned') {
|
||||
usingSameIndentation = tokens.isTab.openTab === tokens.isTab.closeTab;
|
||||
}
|
||||
|
||||
if (hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
location: MESSAGE_LOCATION[expectedLocation],
|
||||
details: '',
|
||||
};
|
||||
const correctColumn = getCorrectColumn(tokens, expectedLocation);
|
||||
|
||||
if (correctColumn !== null) {
|
||||
expectedNextLine = tokens.lastProp
|
||||
&& (tokens.lastProp.lastLine === tokens.closing.line);
|
||||
data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`;
|
||||
}
|
||||
|
||||
report(context, messages.bracketLocation, 'bracketLocation', {
|
||||
node,
|
||||
loc: tokens.closing,
|
||||
data,
|
||||
fix(fixer) {
|
||||
const closingTag = tokens.selfClosing ? '/>' : '>';
|
||||
switch (expectedLocation) {
|
||||
case 'after-tag':
|
||||
if (cachedLastAttributeEndPos) {
|
||||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
||||
(expectedNextLine ? '\n' : '') + closingTag);
|
||||
}
|
||||
return fixer.replaceTextRange([node.name.range[1], node.range[1]],
|
||||
(expectedNextLine ? '\n' : ' ') + closingTag);
|
||||
case 'after-props':
|
||||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
||||
(expectedNextLine ? '\n' : '') + closingTag);
|
||||
case 'props-aligned':
|
||||
case 'tag-aligned':
|
||||
case 'line-aligned':
|
||||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]],
|
||||
`\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
142
node_modules/eslint-plugin-react/lib/rules/jsx-closing-tag-location.js
generated
vendored
Normal file
142
node_modules/eslint-plugin-react/lib/rules/jsx-closing-tag-location.js
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview Validate closing tag location in JSX
|
||||
* @author Ross Solomon
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const repeat = require('string.prototype.repeat');
|
||||
const has = require('hasown');
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
|
||||
matchIndent: 'Expected closing tag to match indentation of opening.',
|
||||
alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
|
||||
};
|
||||
|
||||
const defaultOption = 'tag-aligned';
|
||||
|
||||
const optionMessageMap = {
|
||||
'tag-aligned': 'matchIndent',
|
||||
'line-aligned': 'alignWithOpening',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce closing tag location for multiline JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-closing-tag-location'),
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
messages,
|
||||
schema: [{
|
||||
anyOf: [
|
||||
{
|
||||
enum: ['tag-aligned', 'line-aligned'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: {
|
||||
enum: ['tag-aligned', 'line-aligned'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0];
|
||||
let option = defaultOption;
|
||||
|
||||
if (typeof config === 'string') {
|
||||
option = config;
|
||||
} else if (typeof config === 'object') {
|
||||
if (has(config, 'location')) {
|
||||
option = config.location;
|
||||
}
|
||||
}
|
||||
|
||||
function getIndentation(openingStartOfLine, opening) {
|
||||
if (option === 'line-aligned') return openingStartOfLine.column;
|
||||
if (option === 'tag-aligned') return opening.loc.start.column;
|
||||
}
|
||||
|
||||
function handleClosingElement(node) {
|
||||
if (!node.parent) {
|
||||
return;
|
||||
}
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
const opening = node.parent.openingElement || node.parent.openingFragment;
|
||||
const openingLoc = sourceCode.getFirstToken(opening).loc.start;
|
||||
const openingLine = sourceCode.lines[openingLoc.line - 1];
|
||||
|
||||
const openingStartOfLine = {
|
||||
column: /^\s*/.exec(openingLine)[0].length,
|
||||
line: openingLoc.line,
|
||||
};
|
||||
|
||||
if (opening.loc.start.line === node.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
opening.loc.start.column === node.loc.start.column
|
||||
&& option === 'tag-aligned'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
openingStartOfLine.column === node.loc.start.column
|
||||
&& option === 'line-aligned'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = astUtil.isNodeFirstInLine(context, node)
|
||||
? optionMessageMap[option]
|
||||
: 'onOwnLine';
|
||||
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
loc: node.loc,
|
||||
fix(fixer) {
|
||||
const indent = repeat(
|
||||
' ',
|
||||
getIndentation(openingStartOfLine, opening)
|
||||
);
|
||||
|
||||
if (astUtil.isNodeFirstInLine(context, node)) {
|
||||
return fixer.replaceTextRange(
|
||||
[node.range[0] - node.loc.start.column, node.range[0]],
|
||||
indent
|
||||
);
|
||||
}
|
||||
|
||||
return fixer.insertTextBefore(node, `\n${indent}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXClosingElement: handleClosingElement,
|
||||
JSXClosingFragment: handleClosingElement,
|
||||
};
|
||||
},
|
||||
};
|
||||
421
node_modules/eslint-plugin-react/lib/rules/jsx-curly-brace-presence.js
generated
vendored
Executable file
421
node_modules/eslint-plugin-react/lib/rules/jsx-curly-brace-presence.js
generated
vendored
Executable file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
|
||||
* @author Jacky Ho
|
||||
* @author Simon Lydell
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const arrayIncludes = require('array-includes');
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const OPTION_ALWAYS = 'always';
|
||||
const OPTION_NEVER = 'never';
|
||||
const OPTION_IGNORE = 'ignore';
|
||||
|
||||
const OPTION_VALUES = [
|
||||
OPTION_ALWAYS,
|
||||
OPTION_NEVER,
|
||||
OPTION_IGNORE,
|
||||
];
|
||||
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE };
|
||||
|
||||
const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
|
||||
|
||||
function containsLineTerminators(rawStringValue) {
|
||||
return /[\n\r\u2028\u2029]/.test(rawStringValue);
|
||||
}
|
||||
|
||||
function containsBackslash(rawStringValue) {
|
||||
return arrayIncludes(rawStringValue, '\\');
|
||||
}
|
||||
|
||||
function containsHTMLEntity(rawStringValue) {
|
||||
return HTML_ENTITY_REGEX().test(rawStringValue);
|
||||
}
|
||||
|
||||
function containsOnlyHtmlEntities(rawStringValue) {
|
||||
return rawStringValue.replace(HTML_ENTITY_REGEX(), '').trim() === '';
|
||||
}
|
||||
|
||||
function containsDisallowedJSXTextChars(rawStringValue) {
|
||||
return /[{<>}]/.test(rawStringValue);
|
||||
}
|
||||
|
||||
function containsQuoteCharacters(value) {
|
||||
return /['"]/.test(value);
|
||||
}
|
||||
|
||||
function containsMultilineComment(value) {
|
||||
return /\/\*/.test(value);
|
||||
}
|
||||
|
||||
function escapeDoubleQuotes(rawStringValue) {
|
||||
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function escapeBackslashes(rawStringValue) {
|
||||
return rawStringValue.replace(/\\/g, '\\\\');
|
||||
}
|
||||
|
||||
function needToEscapeCharacterForJSX(raw, node) {
|
||||
return (
|
||||
containsBackslash(raw)
|
||||
|| containsHTMLEntity(raw)
|
||||
|| (node.parent.type !== 'JSXAttribute' && containsDisallowedJSXTextChars(raw))
|
||||
);
|
||||
}
|
||||
|
||||
function containsWhitespaceExpression(child) {
|
||||
if (child.type === 'JSXExpressionContainer') {
|
||||
const value = child.expression.value;
|
||||
return value ? jsxUtil.isWhiteSpaces(value) : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isLineBreak(text) {
|
||||
return containsLineTerminators(text) && text.trim() === '';
|
||||
}
|
||||
|
||||
function wrapNonHTMLEntities(text) {
|
||||
const HTML_ENTITY = '<HTML_ENTITY>';
|
||||
const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => (
|
||||
word === '' ? '' : `{${JSON.stringify(word)}}`
|
||||
)).join(HTML_ENTITY);
|
||||
|
||||
const htmlEntities = text.match(HTML_ENTITY_REGEX());
|
||||
return htmlEntities.reduce((acc, htmlEntity) => (
|
||||
acc.replace(HTML_ENTITY, htmlEntity)
|
||||
), withCurlyBraces);
|
||||
}
|
||||
|
||||
function wrapWithCurlyBraces(rawText) {
|
||||
if (!containsLineTerminators(rawText)) {
|
||||
return `{${JSON.stringify(rawText)}}`;
|
||||
}
|
||||
|
||||
return rawText.split('\n').map((line) => {
|
||||
if (line.trim() === '') {
|
||||
return line;
|
||||
}
|
||||
const firstCharIndex = line.search(/[^\s]/);
|
||||
const leftWhitespace = line.slice(0, firstCharIndex);
|
||||
const text = line.slice(firstCharIndex);
|
||||
|
||||
if (containsHTMLEntity(line)) {
|
||||
return `${leftWhitespace}${wrapNonHTMLEntities(text)}`;
|
||||
}
|
||||
return `${leftWhitespace}{${JSON.stringify(text)}}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function isWhiteSpaceLiteral(node) {
|
||||
return node.type && node.type === 'Literal' && node.value && jsxUtil.isWhiteSpaces(node.value);
|
||||
}
|
||||
|
||||
function isStringWithTrailingWhiteSpaces(value) {
|
||||
return /^\s|\s$/.test(value);
|
||||
}
|
||||
|
||||
function isLiteralWithTrailingWhiteSpaces(node) {
|
||||
return node.type && node.type === 'Literal' && node.value && isStringWithTrailingWhiteSpaces(node.value);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
unnecessaryCurly: 'Curly braces are unnecessary here.',
|
||||
missingCurly: 'Need to wrap this literal in a JSX expression.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-curly-brace-presence'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
props: { enum: OPTION_VALUES },
|
||||
children: { enum: OPTION_VALUES },
|
||||
propElementValues: { enum: OPTION_VALUES },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
enum: OPTION_VALUES,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const ruleOptions = context.options[0];
|
||||
const userConfig = typeof ruleOptions === 'string'
|
||||
? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE }
|
||||
: Object.assign({}, DEFAULT_CONFIG, ruleOptions);
|
||||
|
||||
/**
|
||||
* Report and fix an unnecessary curly brace violation on a node
|
||||
* @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression
|
||||
*/
|
||||
function reportUnnecessaryCurly(JSXExpressionNode) {
|
||||
report(context, messages.unnecessaryCurly, 'unnecessaryCurly', {
|
||||
node: JSXExpressionNode,
|
||||
fix(fixer) {
|
||||
const expression = JSXExpressionNode.expression;
|
||||
|
||||
let textToReplace;
|
||||
if (jsxUtil.isJSX(expression)) {
|
||||
textToReplace = getText(context, expression);
|
||||
} else {
|
||||
const expressionType = expression && expression.type;
|
||||
const parentType = JSXExpressionNode.parent.type;
|
||||
|
||||
if (parentType === 'JSXAttribute') {
|
||||
if (expressionType !== 'TemplateLiteral' && /["]/.test(expression.raw.slice(1, -1))) {
|
||||
textToReplace = expression.raw;
|
||||
} else {
|
||||
textToReplace = `"${expressionType === 'TemplateLiteral'
|
||||
? expression.quasis[0].value.raw
|
||||
: expression.raw.slice(1, -1)
|
||||
}"`;
|
||||
}
|
||||
} else if (jsxUtil.isJSX(expression)) {
|
||||
textToReplace = getText(context, expression);
|
||||
} else {
|
||||
textToReplace = expressionType === 'TemplateLiteral'
|
||||
? expression.quasis[0].value.cooked : expression.value;
|
||||
}
|
||||
}
|
||||
|
||||
return fixer.replaceText(JSXExpressionNode, textToReplace);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function reportMissingCurly(literalNode) {
|
||||
report(context, messages.missingCurly, 'missingCurly', {
|
||||
node: literalNode,
|
||||
fix(fixer) {
|
||||
if (jsxUtil.isJSX(literalNode)) {
|
||||
return fixer.replaceText(literalNode, `{${getText(context, literalNode)}}`);
|
||||
}
|
||||
|
||||
// If a HTML entity name is found, bail out because it can be fixed
|
||||
// by either using the real character or the unicode equivalent.
|
||||
// If it contains any line terminator character, bail out as well.
|
||||
if (
|
||||
containsOnlyHtmlEntities(literalNode.raw)
|
||||
|| (literalNode.parent.type === 'JSXAttribute' && containsLineTerminators(literalNode.raw))
|
||||
|| isLineBreak(literalNode.raw)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expression = literalNode.parent.type === 'JSXAttribute'
|
||||
? `{"${escapeDoubleQuotes(escapeBackslashes(
|
||||
literalNode.raw.slice(1, -1)
|
||||
))}"}`
|
||||
: wrapWithCurlyBraces(literalNode.raw);
|
||||
|
||||
return fixer.replaceText(literalNode, expression);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Bail out if there is any character that needs to be escaped in JSX
|
||||
// because escaping decreases readability and the original code may be more
|
||||
// readable anyway or intentional for other specific reasons
|
||||
function lintUnnecessaryCurly(JSXExpressionNode) {
|
||||
const expression = JSXExpressionNode.expression;
|
||||
const expressionType = expression.type;
|
||||
|
||||
const sourceCode = getSourceCode(context);
|
||||
// Curly braces containing comments are necessary
|
||||
if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(expressionType === 'Literal' || expressionType === 'JSXText')
|
||||
&& typeof expression.value === 'string'
|
||||
&& (
|
||||
(JSXExpressionNode.parent.type === 'JSXAttribute' && !isWhiteSpaceLiteral(expression))
|
||||
|| !isLiteralWithTrailingWhiteSpaces(expression)
|
||||
)
|
||||
&& !containsMultilineComment(expression.value)
|
||||
&& !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (
|
||||
jsxUtil.isJSX(JSXExpressionNode.parent)
|
||||
|| (!containsQuoteCharacters(expression.value) || typeof expression.value === 'string')
|
||||
)
|
||||
) {
|
||||
reportUnnecessaryCurly(JSXExpressionNode);
|
||||
} else if (
|
||||
expressionType === 'TemplateLiteral'
|
||||
&& expression.expressions.length === 0
|
||||
&& expression.quasis[0].value.raw.indexOf('\n') === -1
|
||||
&& !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw)
|
||||
&& !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode)
|
||||
&& !containsQuoteCharacters(expression.quasis[0].value.cooked)
|
||||
) {
|
||||
reportUnnecessaryCurly(JSXExpressionNode);
|
||||
} else if (jsxUtil.isJSX(expression)) {
|
||||
reportUnnecessaryCurly(JSXExpressionNode);
|
||||
}
|
||||
}
|
||||
|
||||
function areRuleConditionsSatisfied(parent, config, ruleCondition) {
|
||||
return (
|
||||
parent.type === 'JSXAttribute'
|
||||
&& typeof config.props === 'string'
|
||||
&& config.props === ruleCondition
|
||||
) || (
|
||||
jsxUtil.isJSX(parent)
|
||||
&& typeof config.children === 'string'
|
||||
&& config.children === ruleCondition
|
||||
);
|
||||
}
|
||||
|
||||
function getAdjacentSiblings(node, children) {
|
||||
for (let i = 1; i < children.length - 1; i++) {
|
||||
const child = children[i];
|
||||
if (node === child) {
|
||||
return [children[i - 1], children[i + 1]];
|
||||
}
|
||||
}
|
||||
if (node === children[0] && children[1]) {
|
||||
return [children[1]];
|
||||
}
|
||||
if (node === children[children.length - 1] && children[children.length - 2]) {
|
||||
return [children[children.length - 2]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasAdjacentJsxExpressionContainers(node, children) {
|
||||
if (!children) {
|
||||
return false;
|
||||
}
|
||||
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
|
||||
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
|
||||
|
||||
return adjSiblings.some((x) => x.type && x.type === 'JSXExpressionContainer');
|
||||
}
|
||||
function hasAdjacentJsx(node, children) {
|
||||
if (!children) {
|
||||
return false;
|
||||
}
|
||||
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
|
||||
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
|
||||
|
||||
return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type));
|
||||
}
|
||||
function shouldCheckForUnnecessaryCurly(node, config) {
|
||||
const parent = node.parent;
|
||||
// Bail out if the parent is a JSXAttribute & its contents aren't
|
||||
// StringLiteral or TemplateLiteral since e.g
|
||||
// <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} />
|
||||
|
||||
if (
|
||||
parent.type && parent.type === 'JSXAttribute'
|
||||
&& (node.expression && node.expression.type
|
||||
&& node.expression.type !== 'Literal'
|
||||
&& node.expression.type !== 'StringLiteral'
|
||||
&& node.expression.type !== 'TemplateLiteral')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are adjacent `JsxExpressionContainer` then there is no need,
|
||||
// to check for unnecessary curly braces.
|
||||
if (jsxUtil.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) {
|
||||
return false;
|
||||
}
|
||||
if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
parent.children
|
||||
&& parent.children.length === 1
|
||||
&& containsWhitespaceExpression(node)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
|
||||
}
|
||||
|
||||
function shouldCheckForMissingCurly(node, config) {
|
||||
if (jsxUtil.isJSX(node)) {
|
||||
return config.propElementValues !== OPTION_IGNORE;
|
||||
}
|
||||
if (
|
||||
isLineBreak(node.raw)
|
||||
|| containsOnlyHtmlEntities(node.raw)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const parent = node.parent;
|
||||
if (
|
||||
parent.children
|
||||
&& parent.children.length === 1
|
||||
&& containsWhitespaceExpression(parent.children[0])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'JSXAttribute > JSXExpressionContainer > JSXElement'(node) {
|
||||
if (userConfig.propElementValues === OPTION_NEVER) {
|
||||
reportUnnecessaryCurly(node.parent);
|
||||
}
|
||||
},
|
||||
|
||||
JSXExpressionContainer(node) {
|
||||
if (shouldCheckForUnnecessaryCurly(node, userConfig)) {
|
||||
lintUnnecessaryCurly(node);
|
||||
}
|
||||
},
|
||||
|
||||
'JSXAttribute > JSXElement, Literal, JSXText'(node) {
|
||||
if (shouldCheckForMissingCurly(node, userConfig)) {
|
||||
reportMissingCurly(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
187
node_modules/eslint-plugin-react/lib/rules/jsx-curly-newline.js
generated
vendored
Normal file
187
node_modules/eslint-plugin-react/lib/rules/jsx-curly-newline.js
generated
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview enforce consistent line breaks inside jsx curly
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const report = require('../util/report');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function getNormalizedOption(context) {
|
||||
const rawOption = context.options[0] || 'consistent';
|
||||
|
||||
if (rawOption === 'consistent') {
|
||||
return {
|
||||
multiline: 'consistent',
|
||||
singleline: 'consistent',
|
||||
};
|
||||
}
|
||||
|
||||
if (rawOption === 'never') {
|
||||
return {
|
||||
multiline: 'forbid',
|
||||
singleline: 'forbid',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
multiline: rawOption.multiline || 'consistent',
|
||||
singleline: rawOption.singleline || 'consistent',
|
||||
};
|
||||
}
|
||||
|
||||
const messages = {
|
||||
expectedBefore: 'Expected newline before \'}\'.',
|
||||
expectedAfter: 'Expected newline after \'{\'.',
|
||||
unexpectedBefore: 'Unexpected newline before \'}\'.',
|
||||
unexpectedAfter: 'Unexpected newline after \'{\'.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'layout',
|
||||
|
||||
docs: {
|
||||
description: 'Enforce consistent linebreaks in curly braces in JSX attributes and expressions',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-curly-newline'),
|
||||
},
|
||||
|
||||
fixable: 'whitespace',
|
||||
|
||||
schema: [
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
enum: ['consistent', 'never'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
singleline: { enum: ['consistent', 'require', 'forbid'] },
|
||||
multiline: { enum: ['consistent', 'require', 'forbid'] },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
messages,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const option = getNormalizedOption(context);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether two adjacent tokens are on the same line.
|
||||
* @param {Object} left - The left token object.
|
||||
* @param {Object} right - The right token object.
|
||||
* @returns {boolean} Whether or not the tokens are on the same line.
|
||||
*/
|
||||
function isTokenOnSameLine(left, right) {
|
||||
return left.loc.end.line === right.loc.start.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether there should be newlines inside curlys
|
||||
* @param {ASTNode} expression The expression contained in the curlys
|
||||
* @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code.
|
||||
* @returns {boolean} `true` if there should be newlines inside the function curlys
|
||||
*/
|
||||
function shouldHaveNewlines(expression, hasLeftNewline) {
|
||||
const isMultiline = expression.loc.start.line !== expression.loc.end.line;
|
||||
|
||||
switch (isMultiline ? option.multiline : option.singleline) {
|
||||
case 'forbid': return false;
|
||||
case 'require': return true;
|
||||
case 'consistent':
|
||||
default: return hasLeftNewline;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates curlys
|
||||
* @param {Object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
|
||||
* @param {ASTNode} expression The expression inside the curly
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateCurlys(curlys, expression) {
|
||||
const leftCurly = curlys.leftCurly;
|
||||
const rightCurly = curlys.rightCurly;
|
||||
const tokenAfterLeftCurly = sourceCode.getTokenAfter(leftCurly);
|
||||
const tokenBeforeRightCurly = sourceCode.getTokenBefore(rightCurly);
|
||||
const hasLeftNewline = !isTokenOnSameLine(leftCurly, tokenAfterLeftCurly);
|
||||
const hasRightNewline = !isTokenOnSameLine(tokenBeforeRightCurly, rightCurly);
|
||||
const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline);
|
||||
|
||||
if (hasLeftNewline && !needsNewlines) {
|
||||
report(context, messages.unexpectedAfter, 'unexpectedAfter', {
|
||||
node: leftCurly,
|
||||
fix(fixer) {
|
||||
return getText(context)
|
||||
.slice(leftCurly.range[1], tokenAfterLeftCurly.range[0])
|
||||
.trim()
|
||||
? null // If there is a comment between the { and the first element, don't do a fix.
|
||||
: fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]);
|
||||
},
|
||||
});
|
||||
} else if (!hasLeftNewline && needsNewlines) {
|
||||
report(context, messages.expectedAfter, 'expectedAfter', {
|
||||
node: leftCurly,
|
||||
fix: (fixer) => fixer.insertTextAfter(leftCurly, '\n'),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasRightNewline && !needsNewlines) {
|
||||
report(context, messages.unexpectedBefore, 'unexpectedBefore', {
|
||||
node: rightCurly,
|
||||
fix(fixer) {
|
||||
return getText(context)
|
||||
.slice(tokenBeforeRightCurly.range[1], rightCurly.range[0])
|
||||
.trim()
|
||||
? null // If there is a comment between the last element and the }, don't do a fix.
|
||||
: fixer.removeRange([
|
||||
tokenBeforeRightCurly.range[1],
|
||||
rightCurly.range[0],
|
||||
]);
|
||||
},
|
||||
});
|
||||
} else if (!hasRightNewline && needsNewlines) {
|
||||
report(context, messages.expectedBefore, 'expectedBefore', {
|
||||
node: rightCurly,
|
||||
fix: (fixer) => fixer.insertTextBefore(rightCurly, '\n'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Public
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXExpressionContainer(node) {
|
||||
const curlyTokens = {
|
||||
leftCurly: sourceCode.getFirstToken(node),
|
||||
rightCurly: sourceCode.getLastToken(node),
|
||||
};
|
||||
validateCurlys(curlyTokens, node.expression);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
432
node_modules/eslint-plugin-react/lib/rules/jsx-curly-spacing.js
generated
vendored
Normal file
432
node_modules/eslint-plugin-react/lib/rules/jsx-curly-spacing.js
generated
vendored
Normal file
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes.
|
||||
* @author Jamund Ferguson
|
||||
* @author Brandyn Bennett
|
||||
* @author Michael Ficarra
|
||||
* @author Vignesh Anand
|
||||
* @author Jamund Ferguson
|
||||
* @author Yannick Croissant
|
||||
* @author Erik Wendel
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const SPACING = {
|
||||
always: 'always',
|
||||
never: 'never',
|
||||
};
|
||||
const SPACING_VALUES = [SPACING.always, SPACING.never];
|
||||
|
||||
const messages = {
|
||||
noNewlineAfter: 'There should be no newline after \'{{token}}\'',
|
||||
noNewlineBefore: 'There should be no newline before \'{{token}}\'',
|
||||
noSpaceAfter: 'There should be no space after \'{{token}}\'',
|
||||
noSpaceBefore: 'There should be no space before \'{{token}}\'',
|
||||
spaceNeededAfter: 'A space is required after \'{{token}}\'',
|
||||
spaceNeededBefore: 'A space is required before \'{{token}}\'',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-curly-spacing'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
basicConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
when: {
|
||||
enum: SPACING_VALUES,
|
||||
},
|
||||
allowMultiline: {
|
||||
type: 'boolean',
|
||||
},
|
||||
spacing: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
objectLiterals: {
|
||||
enum: SPACING_VALUES,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
basicConfigOrBoolean: {
|
||||
anyOf: [{
|
||||
$ref: '#/definitions/basicConfig',
|
||||
}, {
|
||||
type: 'boolean',
|
||||
}],
|
||||
},
|
||||
},
|
||||
type: 'array',
|
||||
items: [{
|
||||
anyOf: [{
|
||||
allOf: [{
|
||||
$ref: '#/definitions/basicConfig',
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
attributes: {
|
||||
$ref: '#/definitions/basicConfigOrBoolean',
|
||||
},
|
||||
children: {
|
||||
$ref: '#/definitions/basicConfigOrBoolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
enum: SPACING_VALUES,
|
||||
}],
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowMultiline: {
|
||||
type: 'boolean',
|
||||
},
|
||||
spacing: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
objectLiterals: {
|
||||
enum: SPACING_VALUES,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function normalizeConfig(configOrTrue, defaults, lastPass) {
|
||||
const config = configOrTrue === true ? {} : configOrTrue;
|
||||
const when = config.when || defaults.when;
|
||||
const allowMultiline = has(config, 'allowMultiline') ? config.allowMultiline : defaults.allowMultiline;
|
||||
const spacing = config.spacing || {};
|
||||
let objectLiteralSpaces = spacing.objectLiterals || defaults.objectLiteralSpaces;
|
||||
if (lastPass) {
|
||||
// On the final pass assign the values that should be derived from others if they are still undefined
|
||||
objectLiteralSpaces = objectLiteralSpaces || when;
|
||||
}
|
||||
|
||||
return {
|
||||
when,
|
||||
allowMultiline,
|
||||
objectLiteralSpaces,
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_WHEN = SPACING.never;
|
||||
const DEFAULT_ALLOW_MULTILINE = true;
|
||||
const DEFAULT_ATTRIBUTES = true;
|
||||
const DEFAULT_CHILDREN = false;
|
||||
|
||||
let originalConfig = context.options[0] || {};
|
||||
if (SPACING_VALUES.indexOf(originalConfig) !== -1) {
|
||||
originalConfig = Object.assign({ when: context.options[0] }, context.options[1]);
|
||||
}
|
||||
const defaultConfig = normalizeConfig(originalConfig, {
|
||||
when: DEFAULT_WHEN,
|
||||
allowMultiline: DEFAULT_ALLOW_MULTILINE,
|
||||
});
|
||||
const attributes = has(originalConfig, 'attributes') ? originalConfig.attributes : DEFAULT_ATTRIBUTES;
|
||||
const attributesConfig = attributes ? normalizeConfig(attributes, defaultConfig, true) : null;
|
||||
const children = has(originalConfig, 'children') ? originalConfig.children : DEFAULT_CHILDREN;
|
||||
const childrenConfig = children ? normalizeConfig(children, defaultConfig, true) : null;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether two adjacent tokens have a newline between them.
|
||||
* @param {Object} left - The left token object.
|
||||
* @param {Object} right - The right token object.
|
||||
* @returns {boolean} Whether or not there is a newline between the tokens.
|
||||
*/
|
||||
function isMultiline(left, right) {
|
||||
return left.loc.end.line !== right.loc.start.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims text of whitespace between two ranges
|
||||
* @param {Fixer} fixer - the eslint fixer object
|
||||
* @param {number} fromLoc - the start location
|
||||
* @param {number} toLoc - the end location
|
||||
* @param {string} mode - either 'start' or 'end'
|
||||
* @param {string=} spacing - a spacing value that will optionally add a space to the removed text
|
||||
* @returns {Object|*|{range, text}}
|
||||
*/
|
||||
function fixByTrimmingWhitespace(fixer, fromLoc, toLoc, mode, spacing) {
|
||||
let replacementText = getSourceCode(context).text.slice(fromLoc, toLoc);
|
||||
if (mode === 'start') {
|
||||
replacementText = replacementText.replace(/^\s+/gm, '');
|
||||
} else {
|
||||
replacementText = replacementText.replace(/\s+$/gm, '');
|
||||
}
|
||||
if (spacing === SPACING.always) {
|
||||
if (mode === 'start') {
|
||||
replacementText += ' ';
|
||||
} else {
|
||||
replacementText = ` ${replacementText}`;
|
||||
}
|
||||
}
|
||||
return fixer.replaceTextRange([fromLoc, toLoc], replacementText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a newline after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @param {string} spacing
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoBeginningNewline(node, token, spacing) {
|
||||
report(context, messages.noNewlineAfter, 'noNewlineAfter', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
const nextToken = getSourceCode(context).getTokenAfter(token);
|
||||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a newline before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @param {string} spacing
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoEndingNewline(node, token, spacing) {
|
||||
report(context, messages.noNewlineBefore, 'noNewlineBefore', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
const previousToken = getSourceCode(context).getTokenBefore(token);
|
||||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoBeginningSpace(node, token) {
|
||||
report(context, messages.noSpaceAfter, 'noSpaceAfter', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
let nextComment;
|
||||
|
||||
// eslint >=4.x
|
||||
if (sourceCode.getCommentsAfter) {
|
||||
nextComment = sourceCode.getCommentsAfter(token);
|
||||
// eslint 3.x
|
||||
} else {
|
||||
const potentialComment = sourceCode.getTokenAfter(token, { includeComments: true });
|
||||
nextComment = nextToken === potentialComment ? [] : [potentialComment];
|
||||
}
|
||||
|
||||
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
|
||||
if (nextComment.length > 0) {
|
||||
return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), 'start');
|
||||
}
|
||||
|
||||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoEndingSpace(node, token) {
|
||||
report(context, messages.noSpaceBefore, 'noSpaceBefore', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const previousToken = sourceCode.getTokenBefore(token);
|
||||
let previousComment;
|
||||
|
||||
// eslint >=4.x
|
||||
if (sourceCode.getCommentsBefore) {
|
||||
previousComment = sourceCode.getCommentsBefore(token);
|
||||
// eslint 3.x
|
||||
} else {
|
||||
const potentialComment = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
previousComment = previousToken === potentialComment ? [] : [potentialComment];
|
||||
}
|
||||
|
||||
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
|
||||
if (previousComment.length > 0) {
|
||||
return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], 'end');
|
||||
}
|
||||
|
||||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredBeginningSpace(node, token) {
|
||||
report(context, messages.spaceNeededAfter, 'spaceNeededAfter', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredEndingSpace(node, token) {
|
||||
report(context, messages.spaceNeededBefore, 'spaceNeededBefore', {
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
data: {
|
||||
token: token.value,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if spacing in curly braces is valid.
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateBraceSpacing(node) {
|
||||
let config;
|
||||
switch (node.parent.type) {
|
||||
case 'JSXAttribute':
|
||||
case 'JSXOpeningElement':
|
||||
config = attributesConfig;
|
||||
break;
|
||||
|
||||
case 'JSXElement':
|
||||
case 'JSXFragment':
|
||||
config = childrenConfig;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (config === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCode = getSourceCode(context);
|
||||
const first = sourceCode.getFirstToken(node);
|
||||
const last = sourceCode.getLastToken(node);
|
||||
let second = sourceCode.getTokenAfter(first, { includeComments: true });
|
||||
let penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
|
||||
|
||||
if (!second) {
|
||||
second = sourceCode.getTokenAfter(first);
|
||||
const leadingComments = sourceCode.getNodeByRangeIndex(second.range[0]).leadingComments;
|
||||
second = leadingComments ? leadingComments[0] : second;
|
||||
}
|
||||
if (!penultimate) {
|
||||
penultimate = sourceCode.getTokenBefore(last);
|
||||
const trailingComments = sourceCode.getNodeByRangeIndex(penultimate.range[0]).trailingComments;
|
||||
penultimate = trailingComments ? trailingComments[trailingComments.length - 1] : penultimate;
|
||||
}
|
||||
|
||||
const isObjectLiteral = first.value === second.value;
|
||||
const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when;
|
||||
if (spacing === SPACING.always) {
|
||||
if (!sourceCode.isSpaceBetweenTokens(first, second)) {
|
||||
reportRequiredBeginningSpace(node, first);
|
||||
} else if (!config.allowMultiline && isMultiline(first, second)) {
|
||||
reportNoBeginningNewline(node, first, spacing);
|
||||
}
|
||||
if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
|
||||
reportRequiredEndingSpace(node, last);
|
||||
} else if (!config.allowMultiline && isMultiline(penultimate, last)) {
|
||||
reportNoEndingNewline(node, last, spacing);
|
||||
}
|
||||
} else if (spacing === SPACING.never) {
|
||||
if (isMultiline(first, second)) {
|
||||
if (!config.allowMultiline) {
|
||||
reportNoBeginningNewline(node, first, spacing);
|
||||
}
|
||||
} else if (sourceCode.isSpaceBetweenTokens(first, second)) {
|
||||
reportNoBeginningSpace(node, first);
|
||||
}
|
||||
if (isMultiline(penultimate, last)) {
|
||||
if (!config.allowMultiline) {
|
||||
reportNoEndingNewline(node, last, spacing);
|
||||
}
|
||||
} else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
|
||||
reportNoEndingSpace(node, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXExpressionContainer: validateBraceSpacing,
|
||||
JSXSpreadAttribute: validateBraceSpacing,
|
||||
};
|
||||
},
|
||||
};
|
||||
112
node_modules/eslint-plugin-react/lib/rules/jsx-equals-spacing.js
generated
vendored
Normal file
112
node_modules/eslint-plugin-react/lib/rules/jsx-equals-spacing.js
generated
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @fileoverview Disallow or enforce spaces around equal signs in JSX attributes.
|
||||
* @author ryym
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noSpaceBefore: 'There should be no space before \'=\'',
|
||||
noSpaceAfter: 'There should be no space after \'=\'',
|
||||
needSpaceBefore: 'A space is required before \'=\'',
|
||||
needSpaceAfter: 'A space is required after \'=\'',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce or disallow spaces around equal signs in JSX attributes',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-equals-spacing'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
enum: ['always', 'never'],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || 'never';
|
||||
|
||||
/**
|
||||
* Determines a given attribute node has an equal sign.
|
||||
* @param {ASTNode} attrNode - The attribute node.
|
||||
* @returns {boolean} Whether or not the attriute node has an equal sign.
|
||||
*/
|
||||
function hasEqual(attrNode) {
|
||||
return attrNode.type !== 'JSXSpreadAttribute' && attrNode.value !== null;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
node.attributes.forEach((attrNode) => {
|
||||
if (!hasEqual(attrNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCode = getSourceCode(context);
|
||||
const equalToken = sourceCode.getTokenAfter(attrNode.name);
|
||||
const spacedBefore = sourceCode.isSpaceBetweenTokens(attrNode.name, equalToken);
|
||||
const spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value);
|
||||
|
||||
if (config === 'never') {
|
||||
if (spacedBefore) {
|
||||
report(context, messages.noSpaceBefore, 'noSpaceBefore', {
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (spacedAfter) {
|
||||
report(context, messages.noSpaceAfter, 'noSpaceAfter', {
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (config === 'always') {
|
||||
if (!spacedBefore) {
|
||||
report(context, messages.needSpaceBefore, 'needSpaceBefore', {
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(equalToken, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!spacedAfter) {
|
||||
report(context, messages.needSpaceAfter, 'needSpaceAfter', {
|
||||
node: attrNode,
|
||||
loc: equalToken.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(equalToken, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
120
node_modules/eslint-plugin-react/lib/rules/jsx-filename-extension.js
generated
vendored
Normal file
120
node_modules/eslint-plugin-react/lib/rules/jsx-filename-extension.js
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @fileoverview Restrict file extensions that may contain JSX
|
||||
* @author Joe Lencioni
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = {
|
||||
allow: 'always',
|
||||
extensions: ['.jsx'],
|
||||
ignoreFilesWithoutCode: false,
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noJSXWithExtension: 'JSX not allowed in files with extension \'{{ext}}\'',
|
||||
extensionOnlyForJSX: 'Only files containing JSX may use the extension \'{{ext}}\'',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow file extensions that may contain JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-filename-extension'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allow: {
|
||||
enum: ['always', 'as-needed'],
|
||||
},
|
||||
extensions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
ignoreFilesWithoutCode: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
let jsxNode;
|
||||
|
||||
if (filename === '<text>') {
|
||||
// No need to traverse any nodes.
|
||||
return {};
|
||||
}
|
||||
|
||||
const allow = (context.options[0] && context.options[0].allow) || DEFAULTS.allow;
|
||||
const allowedExtensions = (context.options[0] && context.options[0].extensions) || DEFAULTS.extensions;
|
||||
const ignoreFilesWithoutCode = (context.options[0] && context.options[0].ignoreFilesWithoutCode)
|
||||
|| DEFAULTS.ignoreFilesWithoutCode;
|
||||
const isAllowedExtension = allowedExtensions.some((extension) => filename.slice(-extension.length) === extension);
|
||||
|
||||
function handleJSX(node) {
|
||||
if (!jsxNode) {
|
||||
jsxNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX,
|
||||
|
||||
'Program:exit'(node) {
|
||||
if (jsxNode) {
|
||||
if (!isAllowedExtension) {
|
||||
report(context, messages.noJSXWithExtension, 'noJSXWithExtension', {
|
||||
node: jsxNode,
|
||||
data: {
|
||||
ext: path.extname(filename),
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAllowedExtension && allow === 'as-needed') {
|
||||
if (ignoreFilesWithoutCode && node.body.length === 0) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.extensionOnlyForJSX, 'extensionOnlyForJSX', {
|
||||
node,
|
||||
data: {
|
||||
ext: path.extname(filename),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
83
node_modules/eslint-plugin-react/lib/rules/jsx-first-prop-new-line.js
generated
vendored
Normal file
83
node_modules/eslint-plugin-react/lib/rules/jsx-first-prop-new-line.js
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview Ensure proper position of the first property in JSX
|
||||
* @author Joachim Seminck
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const propsUtil = require('../util/props');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propOnNewLine: 'Property should be placed on a new line',
|
||||
propOnSameLine: 'Property should be placed on the same line as the component declaration',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce proper position of the first property in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-first-prop-new-line'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
enum: ['always', 'never', 'multiline', 'multiline-multiprop', 'multiprop'],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || 'multiline-multiprop';
|
||||
|
||||
function isMultilineJSX(jsxNode) {
|
||||
return jsxNode.loc.start.line < jsxNode.loc.end.line;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (
|
||||
(configuration === 'multiline' && isMultilineJSX(node))
|
||||
|| (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1)
|
||||
|| (configuration === 'multiprop' && node.attributes.length > 1)
|
||||
|| (configuration === 'always')
|
||||
) {
|
||||
node.attributes.some((decl) => {
|
||||
if (decl.loc.start.line === node.loc.start.line) {
|
||||
report(context, messages.propOnNewLine, 'propOnNewLine', {
|
||||
node: decl,
|
||||
fix(fixer) {
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
return fixer.replaceTextRange([(nodeTypeArguments || node.name).range[1], decl.range[0]], '\n');
|
||||
},
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else if (
|
||||
(configuration === 'never' && node.attributes.length > 0)
|
||||
|| (configuration === 'multiprop' && isMultilineJSX(node) && node.attributes.length <= 1)
|
||||
) {
|
||||
const firstNode = node.attributes[0];
|
||||
if (node.loc.start.line < firstNode.loc.start.line) {
|
||||
report(context, messages.propOnSameLine, 'propOnSameLine', {
|
||||
node: firstNode,
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
208
node_modules/eslint-plugin-react/lib/rules/jsx-fragments.js
generated
vendored
Normal file
208
node_modules/eslint-plugin-react/lib/rules/jsx-fragments.js
generated
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @fileoverview Enforce shorthand or standard form for React fragments.
|
||||
* @author Alex Zherdev
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const variableUtil = require('../util/variable');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function replaceNode(source, node, text) {
|
||||
return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.',
|
||||
preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand',
|
||||
preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce shorthand or standard form for React fragments',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-fragments'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
enum: ['syntax', 'element'],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || 'syntax';
|
||||
const reactPragma = pragmaUtil.getFromContext(context);
|
||||
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
|
||||
const openFragShort = '<>';
|
||||
const closeFragShort = '</>';
|
||||
const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
|
||||
const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;
|
||||
|
||||
function reportOnReactVersion(node) {
|
||||
if (!testReactVersion(context, '>= 16.2.0')) {
|
||||
report(context, messages.fragmentsNotSupported, 'fragmentsNotSupported', {
|
||||
node,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFixerToLong(jsxFragment) {
|
||||
if (!jsxFragment.closingFragment || !jsxFragment.openingFragment) {
|
||||
// the old TS parser crashes here
|
||||
// TODO: FIXME: can we fake these two descriptors?
|
||||
return null;
|
||||
}
|
||||
return function fix(fixer) {
|
||||
let source = getText(context);
|
||||
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
|
||||
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
|
||||
const lengthDiff = openFragLong.length - getText(context, jsxFragment.openingFragment).length
|
||||
+ closeFragLong.length - getText(context, jsxFragment.closingFragment).length;
|
||||
const range = jsxFragment.range;
|
||||
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
|
||||
};
|
||||
}
|
||||
|
||||
function getFixerToShort(jsxElement) {
|
||||
return function fix(fixer) {
|
||||
let source = getText(context);
|
||||
let lengthDiff;
|
||||
if (jsxElement.closingElement) {
|
||||
source = replaceNode(source, jsxElement.closingElement, closeFragShort);
|
||||
source = replaceNode(source, jsxElement.openingElement, openFragShort);
|
||||
lengthDiff = getText(context, jsxElement.openingElement).length - openFragShort.length
|
||||
+ getText(context, jsxElement.closingElement).length - closeFragShort.length;
|
||||
} else {
|
||||
source = replaceNode(source, jsxElement.openingElement, `${openFragShort}${closeFragShort}`);
|
||||
lengthDiff = getText(context, jsxElement.openingElement).length - openFragShort.length
|
||||
- closeFragShort.length;
|
||||
}
|
||||
|
||||
const range = jsxElement.range;
|
||||
return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
|
||||
};
|
||||
}
|
||||
|
||||
function refersToReactFragment(node, name) {
|
||||
const variableInit = variableUtil.findVariableByName(context, node, name);
|
||||
if (!variableInit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// const { Fragment } = React;
|
||||
if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// const Fragment = React.Fragment;
|
||||
if (
|
||||
variableInit.type === 'MemberExpression'
|
||||
&& variableInit.object.type === 'Identifier'
|
||||
&& variableInit.object.name === reactPragma
|
||||
&& variableInit.property.type === 'Identifier'
|
||||
&& variableInit.property.name === fragmentPragma
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// const { Fragment } = require('react');
|
||||
if (
|
||||
variableInit.callee
|
||||
&& variableInit.callee.name === 'require'
|
||||
&& variableInit.arguments
|
||||
&& variableInit.arguments[0]
|
||||
&& variableInit.arguments[0].value === 'react'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsxElements = [];
|
||||
const fragmentNames = new Set([`${reactPragma}.${fragmentPragma}`]);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
jsxElements.push(node);
|
||||
},
|
||||
|
||||
JSXFragment(node) {
|
||||
if (reportOnReactVersion(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration === 'element') {
|
||||
report(context, messages.preferPragma, 'preferPragma', {
|
||||
node,
|
||||
data: {
|
||||
react: reactPragma,
|
||||
fragment: fragmentPragma,
|
||||
},
|
||||
fix: getFixerToLong(node),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
ImportDeclaration(node) {
|
||||
if (node.source && node.source.value === 'react') {
|
||||
node.specifiers.forEach((spec) => {
|
||||
if ('imported' in spec && spec.imported && spec.imported.name === fragmentPragma) {
|
||||
if (spec.local) {
|
||||
fragmentNames.add(spec.local.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
jsxElements.forEach((node) => {
|
||||
const openingEl = node.openingElement;
|
||||
const elName = elementType(openingEl);
|
||||
|
||||
if (fragmentNames.has(elName) || refersToReactFragment(node, elName)) {
|
||||
if (reportOnReactVersion(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attrs = openingEl.attributes;
|
||||
if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
|
||||
report(context, messages.preferFragment, 'preferFragment', {
|
||||
node,
|
||||
data: {
|
||||
react: reactPragma,
|
||||
fragment: fragmentPragma,
|
||||
},
|
||||
fix: getFixerToShort(node),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
209
node_modules/eslint-plugin-react/lib/rules/jsx-handler-names.js
generated
vendored
Normal file
209
node_modules/eslint-plugin-react/lib/rules/jsx-handler-names.js
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @fileoverview Enforce event handler naming conventions in JSX
|
||||
* @author Jake Marsh
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const minimatch = require('minimatch');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
badHandlerName: 'Handler function for {{propKey}} prop key must be a camelCase name beginning with \'{{handlerPrefix}}\' only',
|
||||
badPropKey: 'Prop key for {{propValue}} must begin with \'{{handlerPropPrefix}}\'',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce event handler naming conventions in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-handler-names'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: { type: 'string' },
|
||||
eventHandlerPropPrefix: { type: 'string' },
|
||||
checkLocalVariables: { type: 'boolean' },
|
||||
checkInlineFunction: { type: 'boolean' },
|
||||
ignoreComponentNames: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: { type: 'string' },
|
||||
eventHandlerPropPrefix: {
|
||||
type: 'boolean',
|
||||
enum: [false],
|
||||
},
|
||||
checkLocalVariables: { type: 'boolean' },
|
||||
checkInlineFunction: { type: 'boolean' },
|
||||
ignoreComponentNames: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
eventHandlerPrefix: {
|
||||
type: 'boolean',
|
||||
enum: [false],
|
||||
},
|
||||
eventHandlerPropPrefix: { type: 'string' },
|
||||
checkLocalVariables: { type: 'boolean' },
|
||||
checkInlineFunction: { type: 'boolean' },
|
||||
ignoreComponentNames: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkLocalVariables: { type: 'boolean' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkInlineFunction: { type: 'boolean' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreComponentNames: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function isPrefixDisabled(prefix) {
|
||||
return prefix === false;
|
||||
}
|
||||
|
||||
function isInlineHandler(node) {
|
||||
return node.value.expression.type === 'ArrowFunctionExpression';
|
||||
}
|
||||
|
||||
const configuration = context.options[0] || {};
|
||||
|
||||
const eventHandlerPrefix = isPrefixDisabled(configuration.eventHandlerPrefix)
|
||||
? null
|
||||
: configuration.eventHandlerPrefix || 'handle';
|
||||
const eventHandlerPropPrefix = isPrefixDisabled(configuration.eventHandlerPropPrefix)
|
||||
? null
|
||||
: configuration.eventHandlerPropPrefix || 'on';
|
||||
|
||||
const EVENT_HANDLER_REGEX = !eventHandlerPrefix
|
||||
? null
|
||||
: new RegExp(`^((props\\.${eventHandlerPropPrefix || ''})|((.*\\.)?${eventHandlerPrefix}))[0-9]*[A-Z].*$`);
|
||||
const PROP_EVENT_HANDLER_REGEX = !eventHandlerPropPrefix
|
||||
? null
|
||||
: new RegExp(`^(${eventHandlerPropPrefix}[A-Z].*|ref)$`);
|
||||
|
||||
const checkLocal = !!configuration.checkLocalVariables;
|
||||
|
||||
const checkInlineFunction = !!configuration.checkInlineFunction;
|
||||
|
||||
const ignoreComponentNames = configuration.ignoreComponentNames || [];
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const componentName = node.parent.name.name;
|
||||
|
||||
const isComponentNameIgnored = ignoreComponentNames.some((ignoredComponentNamePattern) => {
|
||||
const isIgnored = minimatch(componentName, ignoredComponentNamePattern);
|
||||
return isIgnored;
|
||||
});
|
||||
|
||||
if (
|
||||
!node.value
|
||||
|| !node.value.expression
|
||||
|| (!checkInlineFunction && isInlineHandler(node))
|
||||
|| (
|
||||
!checkLocal
|
||||
&& (isInlineHandler(node)
|
||||
? !node.value.expression.body.callee || !node.value.expression.body.callee.object
|
||||
: !node.value.expression.object
|
||||
)
|
||||
)
|
||||
|| isComponentNameIgnored
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propKey = typeof node.name === 'object' ? node.name.name : node.name;
|
||||
const expression = node.value.expression;
|
||||
const propValue = getText(
|
||||
context,
|
||||
checkInlineFunction && isInlineHandler(node) ? expression.body.callee : expression
|
||||
).replace(/\s*/g, '').replace(/^this\.|.*::/, '');
|
||||
|
||||
if (propKey === 'ref') {
|
||||
return;
|
||||
}
|
||||
|
||||
const propIsEventHandler = PROP_EVENT_HANDLER_REGEX && PROP_EVENT_HANDLER_REGEX.test(propKey);
|
||||
const propFnIsNamedCorrectly = EVENT_HANDLER_REGEX && EVENT_HANDLER_REGEX.test(propValue);
|
||||
|
||||
if (
|
||||
propIsEventHandler
|
||||
&& propFnIsNamedCorrectly !== null
|
||||
&& !propFnIsNamedCorrectly
|
||||
) {
|
||||
report(context, messages.badHandlerName, 'badHandlerName', {
|
||||
node,
|
||||
data: {
|
||||
propKey,
|
||||
handlerPrefix: eventHandlerPrefix,
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
propFnIsNamedCorrectly
|
||||
&& propIsEventHandler !== null
|
||||
&& !propIsEventHandler
|
||||
) {
|
||||
report(context, messages.badPropKey, 'badPropKey', {
|
||||
node,
|
||||
data: {
|
||||
propValue,
|
||||
handlerPropPrefix: eventHandlerPropPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
218
node_modules/eslint-plugin-react/lib/rules/jsx-indent-props.js
generated
vendored
Normal file
218
node_modules/eslint-plugin-react/lib/rules/jsx-indent-props.js
generated
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @fileoverview Validate props indentation in JSX
|
||||
* @author Yannick Croissant
|
||||
|
||||
* This rule has been ported and modified from eslint and nodeca.
|
||||
* @author Vitaly Puzrin
|
||||
* @author Gyandeep Singh
|
||||
* @copyright 2015 Vitaly Puzrin. All rights reserved.
|
||||
* @copyright 2015 Gyandeep Singh. All rights reserved.
|
||||
Copyright (C) 2014 by Vitaly Puzrin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the 'Software'), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const repeat = require('string.prototype.repeat');
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const reportC = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce props indentation in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-indent-props'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
anyOf: [{
|
||||
enum: ['tab', 'first'],
|
||||
}, {
|
||||
type: 'integer',
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
indentMode: {
|
||||
anyOf: [{
|
||||
enum: ['tab', 'first'],
|
||||
}, {
|
||||
type: 'integer',
|
||||
}],
|
||||
},
|
||||
ignoreTernaryOperator: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const extraColumnStart = 0;
|
||||
let indentType = 'space';
|
||||
/** @type {number|'first'} */
|
||||
let indentSize = 4;
|
||||
const line = {
|
||||
isUsingOperator: false,
|
||||
currentOperator: false,
|
||||
};
|
||||
let ignoreTernaryOperator = false;
|
||||
|
||||
if (context.options.length) {
|
||||
const isConfigObject = typeof context.options[0] === 'object';
|
||||
const indentMode = isConfigObject
|
||||
? context.options[0].indentMode
|
||||
: context.options[0];
|
||||
|
||||
if (indentMode === 'first') {
|
||||
indentSize = 'first';
|
||||
indentType = 'space';
|
||||
} else if (indentMode === 'tab') {
|
||||
indentSize = 1;
|
||||
indentType = 'tab';
|
||||
} else if (typeof indentMode === 'number') {
|
||||
indentSize = indentMode;
|
||||
indentType = 'space';
|
||||
}
|
||||
|
||||
if (isConfigObject && context.options[0].ignoreTernaryOperator) {
|
||||
ignoreTernaryOperator = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given indent violation and properly pluralizes the message
|
||||
* @param {ASTNode} node Node violating the indent rule
|
||||
* @param {number} needed Expected indentation character count
|
||||
* @param {number} gotten Indentation character count in the actual node/code
|
||||
*/
|
||||
function report(node, needed, gotten) {
|
||||
const msgContext = {
|
||||
needed,
|
||||
type: indentType,
|
||||
characters: needed === 1 ? 'character' : 'characters',
|
||||
gotten,
|
||||
};
|
||||
|
||||
reportC(context, messages.wrongIndent, 'wrongIndent', {
|
||||
node,
|
||||
data: msgContext,
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
|
||||
repeat(indentType === 'space' ? ' ' : '\t', needed)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node indent
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @return {number} Indent
|
||||
*/
|
||||
function getNodeIndent(node) {
|
||||
let src = getText(context, node, node.loc.start.column + extraColumnStart);
|
||||
const lines = src.split('\n');
|
||||
src = lines[0];
|
||||
|
||||
let regExp;
|
||||
if (indentType === 'space') {
|
||||
regExp = /^[ ]+/;
|
||||
} else {
|
||||
regExp = /^[\t]+/;
|
||||
}
|
||||
|
||||
const indent = regExp.exec(src);
|
||||
const useOperator = /^([ ]|[\t])*[:]/.test(src) || /^([ ]|[\t])*[?]/.test(src);
|
||||
const useBracket = /[<]/.test(src);
|
||||
|
||||
line.currentOperator = false;
|
||||
if (useOperator) {
|
||||
line.isUsingOperator = true;
|
||||
line.currentOperator = true;
|
||||
} else if (useBracket) {
|
||||
line.isUsingOperator = false;
|
||||
}
|
||||
|
||||
return indent ? indent[0].length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for nodes list
|
||||
* @param {ASTNode[]} nodes list of node objects
|
||||
* @param {number} indent needed indent
|
||||
*/
|
||||
function checkNodesIndent(nodes, indent) {
|
||||
let nestedIndent = indent;
|
||||
nodes.forEach((node) => {
|
||||
const nodeIndent = getNodeIndent(node);
|
||||
if (
|
||||
line.isUsingOperator
|
||||
&& !line.currentOperator
|
||||
&& indentSize !== 'first'
|
||||
&& !ignoreTernaryOperator
|
||||
) {
|
||||
nestedIndent += indentSize;
|
||||
line.isUsingOperator = false;
|
||||
}
|
||||
if (
|
||||
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression'
|
||||
&& nodeIndent !== nestedIndent && astUtil.isNodeFirstInLine(context, node)
|
||||
) {
|
||||
report(node, nestedIndent, nodeIndent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (!node.attributes.length) {
|
||||
return;
|
||||
}
|
||||
let propIndent;
|
||||
if (indentSize === 'first') {
|
||||
const firstPropNode = node.attributes[0];
|
||||
propIndent = firstPropNode.loc.start.column;
|
||||
} else {
|
||||
const elementIndent = getNodeIndent(node);
|
||||
propIndent = elementIndent + indentSize;
|
||||
}
|
||||
checkNodesIndent(node.attributes, propIndent);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
447
node_modules/eslint-plugin-react/lib/rules/jsx-indent.js
generated
vendored
Normal file
447
node_modules/eslint-plugin-react/lib/rules/jsx-indent.js
generated
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* @fileoverview Validate JSX indentation
|
||||
* @author Yannick Croissant
|
||||
|
||||
* This rule has been ported and modified from eslint and nodeca.
|
||||
* @author Vitaly Puzrin
|
||||
* @author Gyandeep Singh
|
||||
* @copyright 2015 Vitaly Puzrin. All rights reserved.
|
||||
* @copyright 2015 Gyandeep Singh. All rights reserved.
|
||||
Copyright (C) 2014 by Vitaly Puzrin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the 'Software'), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const matchAll = require('string.prototype.matchall');
|
||||
const repeat = require('string.prototype.repeat');
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const reportC = require('../util/report');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce JSX indentation',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-indent'),
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
anyOf: [{
|
||||
enum: ['tab'],
|
||||
}, {
|
||||
type: 'integer',
|
||||
}],
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkAttributes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
indentLogicalExpressions: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const extraColumnStart = 0;
|
||||
let indentType = 'space';
|
||||
let indentSize = 4;
|
||||
|
||||
if (context.options.length) {
|
||||
if (context.options[0] === 'tab') {
|
||||
indentSize = 1;
|
||||
indentType = 'tab';
|
||||
} else if (typeof context.options[0] === 'number') {
|
||||
indentSize = context.options[0];
|
||||
indentType = 'space';
|
||||
}
|
||||
}
|
||||
|
||||
const indentChar = indentType === 'space' ? ' ' : '\t';
|
||||
const options = context.options[1] || {};
|
||||
const checkAttributes = options.checkAttributes || false;
|
||||
const indentLogicalExpressions = options.indentLogicalExpressions || false;
|
||||
|
||||
/**
|
||||
* Responsible for fixing the indentation issue fix
|
||||
* @param {ASTNode} node Node violating the indent rule
|
||||
* @param {number} needed Expected indentation character count
|
||||
* @returns {Function} function to be executed by the fixer
|
||||
* @private
|
||||
*/
|
||||
function getFixerFunction(node, needed) {
|
||||
const indent = repeat(indentChar, needed);
|
||||
|
||||
if (node.type === 'JSXText' || node.type === 'Literal') {
|
||||
return function fix(fixer) {
|
||||
const regExp = /\n[\t ]*(\S)/g;
|
||||
const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
|
||||
return fixer.replaceText(node, fixedText);
|
||||
};
|
||||
}
|
||||
|
||||
if (node.type === 'ReturnStatement') {
|
||||
const raw = getText(context, node);
|
||||
const lines = raw.split('\n');
|
||||
if (lines.length > 1) {
|
||||
return function fix(fixer) {
|
||||
const lastLineStart = raw.lastIndexOf('\n');
|
||||
const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`);
|
||||
return fixer.replaceTextRange(
|
||||
[node.range[0] + lastLineStart, node.range[1]],
|
||||
lastLine
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return function fix(fixer) {
|
||||
return fixer.replaceTextRange(
|
||||
[node.range[0] - node.loc.start.column, node.range[0]],
|
||||
indent
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given indent violation and properly pluralizes the message
|
||||
* @param {ASTNode} node Node violating the indent rule
|
||||
* @param {number} needed Expected indentation character count
|
||||
* @param {number} gotten Indentation character count in the actual node/code
|
||||
* @param {Object} [loc] Error line and column location
|
||||
*/
|
||||
function report(node, needed, gotten, loc) {
|
||||
const msgContext = {
|
||||
needed,
|
||||
type: indentType,
|
||||
characters: needed === 1 ? 'character' : 'characters',
|
||||
gotten,
|
||||
};
|
||||
|
||||
reportC(context, messages.wrongIndent, 'wrongIndent', Object.assign({
|
||||
node,
|
||||
data: msgContext,
|
||||
fix: getFixerFunction(node, needed),
|
||||
}, loc && { loc }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node indent
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @param {boolean} [byLastLine] get indent of node's last line
|
||||
* @param {boolean} [excludeCommas] skip comma on start of line
|
||||
* @return {number} Indent
|
||||
*/
|
||||
function getNodeIndent(node, byLastLine, excludeCommas) {
|
||||
let src = getText(context, node, node.loc.start.column + extraColumnStart);
|
||||
const lines = src.split('\n');
|
||||
if (byLastLine) {
|
||||
src = lines[lines.length - 1];
|
||||
} else {
|
||||
src = lines[0];
|
||||
}
|
||||
|
||||
const skip = excludeCommas ? ',' : '';
|
||||
|
||||
let regExp;
|
||||
if (indentType === 'space') {
|
||||
regExp = new RegExp(`^[ ${skip}]+`);
|
||||
} else {
|
||||
regExp = new RegExp(`^[\t${skip}]+`);
|
||||
}
|
||||
|
||||
const indent = regExp.exec(src);
|
||||
return indent ? indent[0].length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is the right member of a logical expression
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {boolean} true if its the case, false if not
|
||||
*/
|
||||
function isRightInLogicalExp(node) {
|
||||
return (
|
||||
node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'LogicalExpression'
|
||||
&& node.parent.parent.right === node.parent
|
||||
&& !indentLogicalExpressions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is the alternate member of a conditional expression
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {boolean} true if its the case, false if not
|
||||
*/
|
||||
function isAlternateInConditionalExp(node) {
|
||||
return (
|
||||
node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'ConditionalExpression'
|
||||
&& node.parent.parent.alternate === node.parent
|
||||
&& getSourceCode(context).getTokenBefore(node).value !== '('
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is within a DoExpression block but not the first expression (which need to be indented)
|
||||
* @param {ASTNode} node The node to check
|
||||
* @return {boolean} true if its the case, false if not
|
||||
*/
|
||||
function isSecondOrSubsequentExpWithinDoExp(node) {
|
||||
/*
|
||||
It returns true when node.parent.parent.parent.parent matches:
|
||||
|
||||
DoExpression({
|
||||
...,
|
||||
body: BlockStatement({
|
||||
...,
|
||||
body: [
|
||||
..., // 1-n times
|
||||
ExpressionStatement({
|
||||
...,
|
||||
expression: JSXElement({
|
||||
...,
|
||||
openingElement: JSXOpeningElement() // the node
|
||||
})
|
||||
}),
|
||||
... // 0-n times
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
except:
|
||||
|
||||
DoExpression({
|
||||
...,
|
||||
body: BlockStatement({
|
||||
...,
|
||||
body: [
|
||||
ExpressionStatement({
|
||||
...,
|
||||
expression: JSXElement({
|
||||
...,
|
||||
openingElement: JSXOpeningElement() // the node
|
||||
})
|
||||
}),
|
||||
... // 0-n times
|
||||
]
|
||||
})
|
||||
})
|
||||
*/
|
||||
const isInExpStmt = (
|
||||
node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'ExpressionStatement'
|
||||
);
|
||||
if (!isInExpStmt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expStmt = node.parent.parent;
|
||||
const isInBlockStmtWithinDoExp = (
|
||||
expStmt.parent
|
||||
&& expStmt.parent.type === 'BlockStatement'
|
||||
&& expStmt.parent.parent
|
||||
&& expStmt.parent.parent.type === 'DoExpression'
|
||||
);
|
||||
if (!isInBlockStmtWithinDoExp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const blockStmt = expStmt.parent;
|
||||
const blockStmtFirstExp = blockStmt.body[0];
|
||||
return !(blockStmtFirstExp === expStmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for nodes list
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {number} indent needed indent
|
||||
* @param {boolean} [excludeCommas] skip comma on start of line
|
||||
*/
|
||||
function checkNodesIndent(node, indent, excludeCommas) {
|
||||
const nodeIndent = getNodeIndent(node, false, excludeCommas);
|
||||
const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
|
||||
const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
|
||||
if (
|
||||
nodeIndent !== indent
|
||||
&& astUtil.isNodeFirstInLine(context, node)
|
||||
&& !isCorrectRightInLogicalExp
|
||||
&& !isCorrectAlternateInCondExp
|
||||
) {
|
||||
report(node, indent, nodeIndent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check indent for Literal Node or JSXText Node
|
||||
* @param {ASTNode} node The node to check
|
||||
* @param {number} indent needed indent
|
||||
*/
|
||||
function checkLiteralNodeIndent(node, indent) {
|
||||
const value = node.value;
|
||||
const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g;
|
||||
const nodeIndentsPerLine = Array.from(
|
||||
matchAll(String(value), regExp),
|
||||
(match) => (match[1] ? match[1].length : 0)
|
||||
);
|
||||
const hasFirstInLineNode = nodeIndentsPerLine.length > 0;
|
||||
if (
|
||||
hasFirstInLineNode
|
||||
&& !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)
|
||||
) {
|
||||
nodeIndentsPerLine.forEach((nodeIndent) => {
|
||||
report(node, indent, nodeIndent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpeningElement(node) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
let prevToken = sourceCode.getTokenBefore(node);
|
||||
if (!prevToken) {
|
||||
return;
|
||||
}
|
||||
// Use the parent in a list or an array
|
||||
if (prevToken.type === 'JSXText' || ((prevToken.type === 'Punctuator') && prevToken.value === ',')) {
|
||||
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
|
||||
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
|
||||
// Use the first non-punctuator token in a conditional expression
|
||||
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
|
||||
do {
|
||||
prevToken = sourceCode.getTokenBefore(prevToken);
|
||||
} while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
|
||||
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
|
||||
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
|
||||
prevToken = prevToken.parent;
|
||||
}
|
||||
}
|
||||
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
|
||||
const parentElementIndent = getNodeIndent(prevToken);
|
||||
const indent = (
|
||||
prevToken.loc.start.line === node.loc.start.line
|
||||
|| isRightInLogicalExp(node)
|
||||
|| isAlternateInConditionalExp(node)
|
||||
|| isSecondOrSubsequentExpWithinDoExp(node)
|
||||
) ? 0 : indentSize;
|
||||
checkNodesIndent(node, parentElementIndent + indent);
|
||||
}
|
||||
|
||||
function handleClosingElement(node) {
|
||||
if (!node.parent) {
|
||||
return;
|
||||
}
|
||||
const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
|
||||
checkNodesIndent(node, peerElementIndent);
|
||||
}
|
||||
|
||||
function handleAttribute(node) {
|
||||
if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
|
||||
return;
|
||||
}
|
||||
const nameIndent = getNodeIndent(node.name);
|
||||
const lastToken = getSourceCode(context).getLastToken(node.value);
|
||||
const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
|
||||
const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
|
||||
checkNodesIndent(firstInLine, indent);
|
||||
}
|
||||
|
||||
function handleLiteral(node) {
|
||||
if (!node.parent) {
|
||||
return;
|
||||
}
|
||||
if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') {
|
||||
return;
|
||||
}
|
||||
const parentNodeIndent = getNodeIndent(node.parent);
|
||||
checkLiteralNodeIndent(node, parentNodeIndent + indentSize);
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement: handleOpeningElement,
|
||||
JSXOpeningFragment: handleOpeningElement,
|
||||
JSXClosingElement: handleClosingElement,
|
||||
JSXClosingFragment: handleClosingElement,
|
||||
JSXAttribute: handleAttribute,
|
||||
JSXExpressionContainer(node) {
|
||||
if (!node.parent) {
|
||||
return;
|
||||
}
|
||||
const parentNodeIndent = getNodeIndent(node.parent);
|
||||
checkNodesIndent(node, parentNodeIndent + indentSize);
|
||||
},
|
||||
Literal: handleLiteral,
|
||||
JSXText: handleLiteral,
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (
|
||||
!node.parent
|
||||
|| !jsxUtil.isJSX(node.argument)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fn = node.parent;
|
||||
while (fn && fn.type !== 'FunctionDeclaration' && fn.type !== 'FunctionExpression') {
|
||||
fn = fn.parent;
|
||||
}
|
||||
if (
|
||||
!fn
|
||||
|| !jsxUtil.isReturningJSX(context, node, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openingIndent = getNodeIndent(node);
|
||||
const closingIndent = getNodeIndent(node, true);
|
||||
|
||||
if (openingIndent !== closingIndent) {
|
||||
report(node, openingIndent, closingIndent);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
303
node_modules/eslint-plugin-react/lib/rules/jsx-key.js
generated
vendored
Normal file
303
node_modules/eslint-plugin-react/lib/rules/jsx-key.js
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* @fileoverview Report missing `key` props in iterators/collection literals.
|
||||
* @author Ben Mosher
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const hasProp = require('jsx-ast-utils/hasProp');
|
||||
const propName = require('jsx-ast-utils/propName');
|
||||
const values = require('object.values');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const report = require('../util/report');
|
||||
const astUtil = require('../util/ast');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const defaultOptions = {
|
||||
checkFragmentShorthand: false,
|
||||
checkKeyMustBeforeSpread: false,
|
||||
warnOnDuplicates: false,
|
||||
};
|
||||
|
||||
const messages = {
|
||||
missingIterKey: 'Missing "key" prop for element in iterator',
|
||||
missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
|
||||
missingArrayKey: 'Missing "key" prop for element in array',
|
||||
missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
|
||||
keyBeforeSpread: '`key` prop must be placed before any `{...spread}, to avoid conflicting with React’s new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`',
|
||||
nonUniqueKeys: '`key` prop must be unique',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing `key` props in iterators/collection literals',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-key'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkFragmentShorthand: {
|
||||
type: 'boolean',
|
||||
default: defaultOptions.checkFragmentShorthand,
|
||||
},
|
||||
checkKeyMustBeforeSpread: {
|
||||
type: 'boolean',
|
||||
default: defaultOptions.checkKeyMustBeforeSpread,
|
||||
},
|
||||
warnOnDuplicates: {
|
||||
type: 'boolean',
|
||||
default: defaultOptions.warnOnDuplicates,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = Object.assign({}, defaultOptions, context.options[0]);
|
||||
const checkFragmentShorthand = options.checkFragmentShorthand;
|
||||
const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
|
||||
const warnOnDuplicates = options.warnOnDuplicates;
|
||||
const reactPragma = pragmaUtil.getFromContext(context);
|
||||
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
|
||||
|
||||
function isKeyAfterSpread(attributes) {
|
||||
let hasFoundSpread = false;
|
||||
return attributes.some((attribute) => {
|
||||
if (attribute.type === 'JSXSpreadAttribute') {
|
||||
hasFoundSpread = true;
|
||||
return false;
|
||||
}
|
||||
if (attribute.type !== 'JSXAttribute') {
|
||||
return false;
|
||||
}
|
||||
return hasFoundSpread && propName(attribute) === 'key';
|
||||
});
|
||||
}
|
||||
|
||||
function checkIteratorElement(node) {
|
||||
if (node.type === 'JSXElement') {
|
||||
if (!hasProp(node.openingElement.attributes, 'key')) {
|
||||
report(context, messages.missingIterKey, 'missingIterKey', { node });
|
||||
} else {
|
||||
const attrs = node.openingElement.attributes;
|
||||
|
||||
if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
|
||||
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { node });
|
||||
}
|
||||
}
|
||||
} else if (checkFragmentShorthand && node.type === 'JSXFragment') {
|
||||
report(context, messages.missingIterKeyUsePrag, 'missingIterKeyUsePrag', {
|
||||
node,
|
||||
data: {
|
||||
reactPrag: reactPragma,
|
||||
fragPrag: fragmentPragma,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getReturnStatements(node) {
|
||||
const returnStatements = arguments[1] || [];
|
||||
if (node.type === 'IfStatement') {
|
||||
if (node.consequent) {
|
||||
getReturnStatements(node.consequent, returnStatements);
|
||||
}
|
||||
if (node.alternate) {
|
||||
getReturnStatements(node.alternate, returnStatements);
|
||||
}
|
||||
} else if (node.type === 'ReturnStatement') {
|
||||
returnStatements.push(node);
|
||||
} else if (Array.isArray(node.body)) {
|
||||
node.body.forEach((item) => {
|
||||
if (item.type === 'IfStatement') {
|
||||
getReturnStatements(item, returnStatements);
|
||||
}
|
||||
|
||||
if (item.type === 'ReturnStatement') {
|
||||
returnStatements.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return returnStatements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a function expression or arrow function,
|
||||
* and checks if there is a missing key prop in return statement's arguments
|
||||
* @param {ASTNode} node
|
||||
*/
|
||||
function checkFunctionsBlockStatement(node) {
|
||||
if (astUtil.isFunctionLikeExpression(node)) {
|
||||
if (node.body.type === 'BlockStatement') {
|
||||
getReturnStatements(node.body)
|
||||
.filter((returnStatement) => returnStatement && returnStatement.argument)
|
||||
.forEach((returnStatement) => {
|
||||
checkIteratorElement(returnStatement.argument);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
|
||||
* and the JSX is missing a key prop
|
||||
* @param {ASTNode} node
|
||||
*/
|
||||
function checkArrowFunctionWithJSX(node) {
|
||||
const isArrFn = node && node.type === 'ArrowFunctionExpression';
|
||||
const shouldCheckNode = (n) => n && (n.type === 'JSXElement' || n.type === 'JSXFragment');
|
||||
if (isArrFn && shouldCheckNode(node.body)) {
|
||||
checkIteratorElement(node.body);
|
||||
}
|
||||
if (node.body.type === 'ConditionalExpression') {
|
||||
if (shouldCheckNode(node.body.consequent)) {
|
||||
checkIteratorElement(node.body.consequent);
|
||||
}
|
||||
if (shouldCheckNode(node.body.alternate)) {
|
||||
checkIteratorElement(node.body.alternate);
|
||||
}
|
||||
} else if (node.body.type === 'LogicalExpression' && shouldCheckNode(node.body.right)) {
|
||||
checkIteratorElement(node.body.right);
|
||||
}
|
||||
}
|
||||
|
||||
const childrenToArraySelector = `:matches(
|
||||
CallExpression
|
||||
[callee.object.object.name=${reactPragma}]
|
||||
[callee.object.property.name=Children]
|
||||
[callee.property.name=toArray],
|
||||
CallExpression
|
||||
[callee.object.name=Children]
|
||||
[callee.property.name=toArray]
|
||||
)`.replace(/\s/g, '');
|
||||
let isWithinChildrenToArray = false;
|
||||
|
||||
const seen = new WeakSet();
|
||||
|
||||
return {
|
||||
[childrenToArraySelector]() {
|
||||
isWithinChildrenToArray = true;
|
||||
},
|
||||
|
||||
[`${childrenToArraySelector}:exit`]() {
|
||||
isWithinChildrenToArray = false;
|
||||
},
|
||||
|
||||
'ArrayExpression, JSXElement > JSXElement'(node) {
|
||||
if (isWithinChildrenToArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement');
|
||||
if (jsx.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const map = {};
|
||||
jsx.forEach((element) => {
|
||||
const attrs = element.openingElement.attributes;
|
||||
const keys = attrs.filter((x) => x.name && x.name.name === 'key');
|
||||
|
||||
if (keys.length === 0) {
|
||||
if (node.type === 'ArrayExpression') {
|
||||
report(context, messages.missingArrayKey, 'missingArrayKey', {
|
||||
node: element,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
keys.forEach((attr) => {
|
||||
const value = getText(context, attr.value);
|
||||
if (!map[value]) { map[value] = []; }
|
||||
map[value].push(attr);
|
||||
|
||||
if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
|
||||
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
|
||||
node: node.type === 'ArrayExpression' ? node : node.parent,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (warnOnDuplicates) {
|
||||
values(map).filter((v) => v.length > 1).forEach((v) => {
|
||||
v.forEach((n) => {
|
||||
if (!seen.has(n)) {
|
||||
seen.add(n);
|
||||
report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
|
||||
node: n,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
JSXFragment(node) {
|
||||
if (!checkFragmentShorthand || isWithinChildrenToArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent.type === 'ArrayExpression') {
|
||||
report(context, messages.missingArrayKeyUsePrag, 'missingArrayKeyUsePrag', {
|
||||
node,
|
||||
data: {
|
||||
reactPrag: reactPragma,
|
||||
fragPrag: fragmentPragma,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Array.prototype.map
|
||||
// eslint-disable-next-line no-multi-str
|
||||
'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
|
||||
CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\
|
||||
OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
|
||||
OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
|
||||
if (isWithinChildrenToArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = node.arguments.length > 0 && node.arguments[0];
|
||||
if (!fn || !astUtil.isFunctionLikeExpression(fn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkArrowFunctionWithJSX(fn);
|
||||
|
||||
checkFunctionsBlockStatement(fn);
|
||||
},
|
||||
|
||||
// Array.from
|
||||
'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
|
||||
if (isWithinChildrenToArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = node.arguments.length > 1 && node.arguments[1];
|
||||
if (!astUtil.isFunctionLikeExpression(fn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkArrowFunctionWithJSX(fn);
|
||||
|
||||
checkFunctionsBlockStatement(fn);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
162
node_modules/eslint-plugin-react/lib/rules/jsx-max-depth.js
generated
vendored
Normal file
162
node_modules/eslint-plugin-react/lib/rules/jsx-max-depth.js
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @fileoverview Validate JSX maximum depth
|
||||
* @author Chris<wfsr@foxmail.com>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const includes = require('array-includes');
|
||||
const variableUtil = require('../util/variable');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const reportC = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
wrongDepth: 'Expected the depth of nested jsx elements to be <= {{needed}}, but found {{found}}.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce JSX maximum depth',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-max-depth'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
max: {
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const DEFAULT_DEPTH = 2;
|
||||
|
||||
const option = context.options[0] || {};
|
||||
const maxDepth = has(option, 'max') ? option.max : DEFAULT_DEPTH;
|
||||
|
||||
function isExpression(node) {
|
||||
return node.type === 'JSXExpressionContainer';
|
||||
}
|
||||
|
||||
function hasJSX(node) {
|
||||
return jsxUtil.isJSX(node) || (isExpression(node) && jsxUtil.isJSX(node.expression));
|
||||
}
|
||||
|
||||
function isLeaf(node) {
|
||||
const children = node.children;
|
||||
|
||||
return !children || children.length === 0 || !children.some(hasJSX);
|
||||
}
|
||||
|
||||
function getDepth(node) {
|
||||
let count = 0;
|
||||
|
||||
while (jsxUtil.isJSX(node.parent) || isExpression(node.parent)) {
|
||||
node = node.parent;
|
||||
if (jsxUtil.isJSX(node)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function report(node, depth) {
|
||||
reportC(context, messages.wrongDepth, 'wrongDepth', {
|
||||
node,
|
||||
data: {
|
||||
found: depth,
|
||||
needed: maxDepth,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function findJSXElementOrFragment(startNode, name, previousReferences) {
|
||||
function find(refs, prevRefs) {
|
||||
for (let i = refs.length - 1; i >= 0; i--) {
|
||||
if (typeof refs[i].writeExpr !== 'undefined') {
|
||||
const writeExpr = refs[i].writeExpr;
|
||||
|
||||
return (jsxUtil.isJSX(writeExpr)
|
||||
&& writeExpr)
|
||||
|| ((writeExpr && writeExpr.type === 'Identifier')
|
||||
&& findJSXElementOrFragment(startNode, writeExpr.name, prevRefs));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const variable = variableUtil.getVariableFromContext(context, startNode, name);
|
||||
if (variable && variable.references) {
|
||||
const containDuplicates = previousReferences.some((ref) => includes(variable.references, ref));
|
||||
|
||||
// Prevent getting stuck in circular references
|
||||
if (containDuplicates) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return find(variable.references, previousReferences.concat(variable.references));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkDescendant(baseDepth, children) {
|
||||
baseDepth += 1;
|
||||
(children || []).filter((node) => hasJSX(node)).forEach((node) => {
|
||||
if (baseDepth > maxDepth) {
|
||||
report(node, baseDepth);
|
||||
} else if (!isLeaf(node)) {
|
||||
checkDescendant(baseDepth, node.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleJSX(node) {
|
||||
if (!isLeaf(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const depth = getDepth(node);
|
||||
if (depth > maxDepth) {
|
||||
report(node, depth);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX,
|
||||
|
||||
JSXExpressionContainer(node) {
|
||||
if (node.expression.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = findJSXElementOrFragment(node, node.expression.name, []);
|
||||
|
||||
if (element) {
|
||||
const baseDepth = getDepth(node);
|
||||
checkDescendant(baseDepth, element.children);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
155
node_modules/eslint-plugin-react/lib/rules/jsx-max-props-per-line.js
generated
vendored
Normal file
155
node_modules/eslint-plugin-react/lib/rules/jsx-max-props-per-line.js
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @fileoverview Limit maximum of props on a single line in JSX
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const report = require('../util/report');
|
||||
|
||||
function getPropName(context, propNode) {
|
||||
if (propNode.type === 'JSXSpreadAttribute') {
|
||||
return getText(context, propNode.argument);
|
||||
}
|
||||
return propNode.name.name;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
newLine: 'Prop `{{prop}}` must be placed on a new line',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce maximum of props on a single line in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-max-props-per-line'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
anyOf: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
maximum: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
single: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
multi: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
maximum: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
},
|
||||
when: {
|
||||
type: 'string',
|
||||
enum: ['always', 'multiline'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const maximum = configuration.maximum || 1;
|
||||
|
||||
const maxConfig = typeof maximum === 'number'
|
||||
? {
|
||||
single: configuration.when === 'multiline' ? Infinity : maximum,
|
||||
multi: maximum,
|
||||
}
|
||||
: {
|
||||
single: maximum.single || Infinity,
|
||||
multi: maximum.multi || Infinity,
|
||||
};
|
||||
|
||||
function generateFixFunction(line, max) {
|
||||
const output = [];
|
||||
const front = line[0].range[0];
|
||||
const back = line[line.length - 1].range[1];
|
||||
|
||||
for (let i = 0; i < line.length; i += max) {
|
||||
const nodes = line.slice(i, i + max);
|
||||
output.push(nodes.reduce((prev, curr) => {
|
||||
if (prev === '') {
|
||||
return getText(context, curr);
|
||||
}
|
||||
return `${prev} ${getText(context, curr)}`;
|
||||
}, ''));
|
||||
}
|
||||
|
||||
const code = output.join('\n');
|
||||
|
||||
return function fix(fixer) {
|
||||
return fixer.replaceTextRange([front, back], code);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (!node.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSingleLineTag = node.loc.start.line === node.loc.end.line;
|
||||
|
||||
if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstProp = node.attributes[0];
|
||||
const linePartitionedProps = [[firstProp]];
|
||||
|
||||
node.attributes.reduce((last, decl) => {
|
||||
if (last.loc.end.line === decl.loc.start.line) {
|
||||
linePartitionedProps[linePartitionedProps.length - 1].push(decl);
|
||||
} else {
|
||||
linePartitionedProps.push([decl]);
|
||||
}
|
||||
return decl;
|
||||
});
|
||||
|
||||
linePartitionedProps.forEach((propsInLine) => {
|
||||
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line
|
||||
? maxConfig.single
|
||||
: maxConfig.multi;
|
||||
|
||||
if (propsInLine.length > maxPropsCountPerLine) {
|
||||
const name = getPropName(context, propsInLine[maxPropsCountPerLine]);
|
||||
report(context, messages.newLine, 'newLine', {
|
||||
node: propsInLine[maxPropsCountPerLine],
|
||||
data: {
|
||||
prop: name,
|
||||
},
|
||||
fix: generateFixFunction(propsInLine, maxPropsCountPerLine),
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
166
node_modules/eslint-plugin-react/lib/rules/jsx-newline.js
generated
vendored
Normal file
166
node_modules/eslint-plugin-react/lib/rules/jsx-newline.js
generated
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @fileoverview Require or prevent a new line after jsx elements and expressions.
|
||||
* @author Johnny Zabala
|
||||
* @author Joseph Stiles
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
require: 'JSX element should start in a new line',
|
||||
prevent: 'JSX element should not start in a new line',
|
||||
allowMultilines: 'Multiline JSX elements should start in a new line',
|
||||
};
|
||||
|
||||
function isMultilined(node) {
|
||||
return node && node.loc.start.line !== node.loc.end.line;
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Require or prevent a new line after jsx elements and expressions.',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-newline'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
prevent: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
allowMultilines: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
if: {
|
||||
properties: {
|
||||
allowMultilines: {
|
||||
const: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
then: {
|
||||
properties: {
|
||||
prevent: {
|
||||
const: true,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'prevent',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const jsxElementParents = new Set();
|
||||
|
||||
function isBlockCommentInCurlyBraces(element) {
|
||||
const elementRawValue = getText(context, element);
|
||||
return /^\s*{\/\*/.test(elementRawValue);
|
||||
}
|
||||
|
||||
function isNonBlockComment(element) {
|
||||
return !isBlockCommentInCurlyBraces(element) && (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer');
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
jsxElementParents.forEach((parent) => {
|
||||
parent.children.forEach((element, index, elements) => {
|
||||
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
|
||||
const configuration = context.options[0] || {};
|
||||
const prevent = configuration.prevent || false;
|
||||
const allowMultilines = configuration.allowMultilines || false;
|
||||
|
||||
const firstAdjacentSibling = elements[index + 1];
|
||||
const secondAdjacentSibling = elements[index + 2];
|
||||
|
||||
const hasSibling = firstAdjacentSibling
|
||||
&& secondAdjacentSibling
|
||||
&& (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText');
|
||||
|
||||
if (!hasSibling) return;
|
||||
|
||||
// Check adjacent sibling has the proper amount of newlines
|
||||
const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value);
|
||||
|
||||
if (isBlockCommentInCurlyBraces(element)) return;
|
||||
if (
|
||||
allowMultilines
|
||||
&& (
|
||||
isMultilined(element)
|
||||
|| isMultilined(elements.slice(index + 2).find(isNonBlockComment))
|
||||
)
|
||||
) {
|
||||
if (!isWithoutNewLine) return;
|
||||
|
||||
const regex = /(\n)(?!.*\1)/g;
|
||||
const replacement = '\n\n';
|
||||
const messageId = 'allowMultilines';
|
||||
|
||||
report(context, messages[messageId], messageId, {
|
||||
node: secondAdjacentSibling,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(
|
||||
firstAdjacentSibling,
|
||||
getText(context, firstAdjacentSibling).replace(regex, replacement)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWithoutNewLine === prevent) return;
|
||||
|
||||
const messageId = prevent
|
||||
? 'prevent'
|
||||
: 'require';
|
||||
|
||||
const regex = prevent
|
||||
? /(\n\n)(?!.*\1)/g
|
||||
: /(\n)(?!.*\1)/g;
|
||||
|
||||
const replacement = prevent
|
||||
? '\n'
|
||||
: '\n\n';
|
||||
|
||||
report(context, messages[messageId], messageId, {
|
||||
node: secondAdjacentSibling,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(
|
||||
firstAdjacentSibling,
|
||||
// double or remove the last newline
|
||||
getText(context, firstAdjacentSibling).replace(regex, replacement)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
|
||||
jsxElementParents.add(node.parent);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
209
node_modules/eslint-plugin-react/lib/rules/jsx-no-bind.js
generated
vendored
Normal file
209
node_modules/eslint-plugin-react/lib/rules/jsx-no-bind.js
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @fileoverview Prevents usage of Function.prototype.bind and arrow functions
|
||||
* in React component props.
|
||||
* @author Daniel Lo Nigro <dan.cx>
|
||||
* @author Jacky Ho
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const propName = require('jsx-ast-utils/propName');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
const getAncestors = require('../util/eslint').getAncestors;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
bindCall: 'JSX props should not use .bind()',
|
||||
arrowFunc: 'JSX props should not use arrow functions',
|
||||
bindExpression: 'JSX props should not use ::',
|
||||
func: 'JSX props should not use functions',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow `.bind()` or arrow functions in JSX props',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-bind'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowArrowFunctions: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
allowBind: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
allowFunctions: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
ignoreRefs: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
ignoreDOMComponents: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
|
||||
// Keep track of all the variable names pointing to a bind call,
|
||||
// bind expression or an arrow function in different block statements
|
||||
const blockVariableNameSets = {};
|
||||
|
||||
/**
|
||||
* @param {string | number} blockStart
|
||||
*/
|
||||
function setBlockVariableNameSet(blockStart) {
|
||||
blockVariableNameSets[blockStart] = {
|
||||
arrowFunc: new Set(),
|
||||
bindCall: new Set(),
|
||||
bindExpression: new Set(),
|
||||
func: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeViolationType(node) {
|
||||
if (
|
||||
!configuration.allowBind
|
||||
&& astUtil.isCallExpression(node)
|
||||
&& node.callee.type === 'MemberExpression'
|
||||
&& node.callee.property.type === 'Identifier'
|
||||
&& node.callee.property.name === 'bind'
|
||||
) {
|
||||
return 'bindCall';
|
||||
}
|
||||
if (node.type === 'ConditionalExpression') {
|
||||
return getNodeViolationType(node.test)
|
||||
|| getNodeViolationType(node.consequent)
|
||||
|| getNodeViolationType(node.alternate);
|
||||
}
|
||||
if (!configuration.allowArrowFunctions && node.type === 'ArrowFunctionExpression') {
|
||||
return 'arrowFunc';
|
||||
}
|
||||
if (
|
||||
!configuration.allowFunctions
|
||||
&& (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration')
|
||||
) {
|
||||
return 'func';
|
||||
}
|
||||
if (!configuration.allowBind && node.type === 'BindExpression') {
|
||||
return 'bindExpression';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} violationType
|
||||
* @param {unknown} variableName
|
||||
* @param {string | number} blockStart
|
||||
*/
|
||||
function addVariableNameToSet(violationType, variableName, blockStart) {
|
||||
blockVariableNameSets[blockStart][violationType].add(variableName);
|
||||
}
|
||||
|
||||
function getBlockStatementAncestors(node) {
|
||||
return getAncestors(context, node).filter(
|
||||
(ancestor) => ancestor.type === 'BlockStatement'
|
||||
).reverse();
|
||||
}
|
||||
|
||||
function reportVariableViolation(node, name, blockStart) {
|
||||
const blockSets = blockVariableNameSets[blockStart];
|
||||
const violationTypes = Object.keys(blockSets);
|
||||
|
||||
return violationTypes.find((type) => {
|
||||
if (blockSets[type].has(name)) {
|
||||
report(context, messages[type], type, {
|
||||
node,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function findVariableViolation(node, name) {
|
||||
getBlockStatementAncestors(node).find(
|
||||
(block) => reportVariableViolation(node, name, block.range[0])
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
BlockStatement(node) {
|
||||
setBlockVariableNameSet(node.range[0]);
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
const blockAncestors = getBlockStatementAncestors(node);
|
||||
const variableViolationType = getNodeViolationType(node);
|
||||
|
||||
if (blockAncestors.length > 0 && variableViolationType) {
|
||||
addVariableNameToSet(variableViolationType, node.id.name, blockAncestors[0].range[0]);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (!node.init) {
|
||||
return;
|
||||
}
|
||||
const blockAncestors = getBlockStatementAncestors(node);
|
||||
const variableViolationType = getNodeViolationType(node.init);
|
||||
|
||||
if (
|
||||
blockAncestors.length > 0
|
||||
&& variableViolationType
|
||||
&& 'kind' in node.parent
|
||||
&& node.parent.kind === 'const' // only support const right now
|
||||
) {
|
||||
addVariableNameToSet(variableViolationType, 'name' in node.id ? node.id.name : undefined, blockAncestors[0].range[0]);
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
const isRef = configuration.ignoreRefs && propName(node) === 'ref';
|
||||
if (isRef || !node.value || !node.value.expression) {
|
||||
return;
|
||||
}
|
||||
const isDOMComponent = jsxUtil.isDOMComponent(node.parent);
|
||||
if (configuration.ignoreDOMComponents && isDOMComponent) {
|
||||
return;
|
||||
}
|
||||
const valueNode = node.value.expression;
|
||||
const valueNodeType = valueNode.type;
|
||||
const nodeViolationType = getNodeViolationType(valueNode);
|
||||
|
||||
if (valueNodeType === 'Identifier') {
|
||||
findVariableViolation(node, valueNode.name);
|
||||
} else if (nodeViolationType) {
|
||||
report(context, messages[nodeViolationType], nodeViolationType, {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
71
node_modules/eslint-plugin-react/lib/rules/jsx-no-comment-textnodes.js
generated
vendored
Normal file
71
node_modules/eslint-plugin-react/lib/rules/jsx-no-comment-textnodes.js
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @fileoverview Comments inside children section of tag should be placed inside braces.
|
||||
* @author Ben Vinegar
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
putCommentInBraces: 'Comments inside children section of tag should be placed inside braces',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
* @param {ASTNode} node
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkText(context, node) {
|
||||
// since babel-eslint has the wrong node.raw, we'll get the source text
|
||||
const rawValue = getText(context, node);
|
||||
if (/^\s*\/(\/|\*)/m.test(rawValue)) {
|
||||
// inside component, e.g. <div>literal</div>
|
||||
if (
|
||||
node.parent.type !== 'JSXAttribute'
|
||||
&& node.parent.type !== 'JSXExpressionContainer'
|
||||
&& node.parent.type.indexOf('JSX') !== -1
|
||||
) {
|
||||
report(context, messages.putCommentInBraces, 'putCommentInBraces', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow comments from being inserted as text nodes',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-no-comment-textnodes'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Literal(node) {
|
||||
checkText(context, node);
|
||||
},
|
||||
JSXText(node) {
|
||||
checkText(context, node);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
227
node_modules/eslint-plugin-react/lib/rules/jsx-no-constructed-context-values.js
generated
vendored
Normal file
227
node_modules/eslint-plugin-react/lib/rules/jsx-no-constructed-context-values.js
generated
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @fileoverview Prevents jsx context provider values from taking values that
|
||||
* will cause needless rerenders.
|
||||
* @author Dylan Oshima
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getScope = require('../util/eslint').getScope;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
// Recursively checks if an element is a construction.
|
||||
// A construction is a variable that changes identity every render.
|
||||
function isConstruction(node, callScope) {
|
||||
switch (node.type) {
|
||||
case 'Literal':
|
||||
if (node.regex != null) {
|
||||
return { type: 'regular expression', node };
|
||||
}
|
||||
return null;
|
||||
case 'Identifier': {
|
||||
const variableScoping = callScope.set.get(node.name);
|
||||
|
||||
if (variableScoping == null || variableScoping.defs == null) {
|
||||
// If it's not in scope, we don't care.
|
||||
return null; // Handled
|
||||
}
|
||||
|
||||
// Gets the last variable identity
|
||||
const variableDefs = variableScoping.defs;
|
||||
const def = variableDefs[variableDefs.length - 1];
|
||||
if (def != null
|
||||
&& def.type !== 'Variable'
|
||||
&& def.type !== 'FunctionName'
|
||||
) {
|
||||
// Parameter or an unusual pattern. Bail out.
|
||||
return null; // Unhandled
|
||||
}
|
||||
|
||||
if (def.node.type === 'FunctionDeclaration') {
|
||||
return { type: 'function declaration', node: def.node, usage: node };
|
||||
}
|
||||
|
||||
const init = def.node.init;
|
||||
if (init == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initConstruction = isConstruction(init, callScope);
|
||||
if (initConstruction == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: initConstruction.type,
|
||||
node: initConstruction.node,
|
||||
usage: node,
|
||||
};
|
||||
}
|
||||
case 'ObjectExpression':
|
||||
// Any object initialized inline will create a new identity
|
||||
return { type: 'object', node };
|
||||
case 'ArrayExpression':
|
||||
return { type: 'array', node };
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'FunctionExpression':
|
||||
// Functions that are initialized inline will have a new identity
|
||||
return { type: 'function expression', node };
|
||||
case 'ClassExpression':
|
||||
return { type: 'class expression', node };
|
||||
case 'NewExpression':
|
||||
// `const a = new SomeClass();` is a construction
|
||||
return { type: 'new expression', node };
|
||||
case 'ConditionalExpression':
|
||||
return (isConstruction(node.consequent, callScope)
|
||||
|| isConstruction(node.alternate, callScope)
|
||||
);
|
||||
case 'LogicalExpression':
|
||||
return (isConstruction(node.left, callScope)
|
||||
|| isConstruction(node.right, callScope)
|
||||
);
|
||||
case 'MemberExpression': {
|
||||
const objConstruction = isConstruction(node.object, callScope);
|
||||
if (objConstruction == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: objConstruction.type,
|
||||
node: objConstruction.node,
|
||||
usage: node.object,
|
||||
};
|
||||
}
|
||||
case 'JSXFragment':
|
||||
return { type: 'JSX fragment', node };
|
||||
case 'JSXElement':
|
||||
return { type: 'JSX element', node };
|
||||
case 'AssignmentExpression': {
|
||||
const construct = isConstruction(node.right, callScope);
|
||||
if (construct != null) {
|
||||
return {
|
||||
type: 'assignment expression',
|
||||
node: construct.node,
|
||||
usage: node,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'TypeCastExpression':
|
||||
case 'TSAsExpression':
|
||||
return isConstruction(node.expression, callScope);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
withIdentifierMsg: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.",
|
||||
withIdentifierMsgFunc: "The '{{variableName}}' {{type}} (at line {{nodeLine}}) passed as the value prop to the Context provider (at line {{usageLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.",
|
||||
defaultMsg: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useMemo hook.',
|
||||
defaultMsgFunc: 'The {{type}} passed as the value prop to the Context provider (at line {{nodeLine}}) changes every render. To fix this consider wrapping it in a useCallback hook.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallows JSX context provider values from taking values that will cause needless rerenders',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-constructed-context-values'),
|
||||
},
|
||||
messages,
|
||||
schema: false,
|
||||
},
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
create: Components.detect((context, components, utils) => {
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
const openingElementName = node.name;
|
||||
if (openingElementName.type !== 'JSXMemberExpression') {
|
||||
// Has no member
|
||||
return;
|
||||
}
|
||||
|
||||
const isJsxContext = openingElementName.property.name === 'Provider';
|
||||
if (!isJsxContext) {
|
||||
// Member is not Provider
|
||||
return;
|
||||
}
|
||||
|
||||
// Contexts can take in more than just a value prop
|
||||
// so we need to iterate through all of them
|
||||
const jsxValueAttribute = node.attributes.find(
|
||||
(attribute) => attribute.type === 'JSXAttribute' && attribute.name.name === 'value'
|
||||
);
|
||||
|
||||
if (jsxValueAttribute == null) {
|
||||
// No value prop was passed
|
||||
return;
|
||||
}
|
||||
|
||||
const valueNode = jsxValueAttribute.value;
|
||||
if (!valueNode) {
|
||||
// attribute is a boolean shorthand
|
||||
return;
|
||||
}
|
||||
if (valueNode.type !== 'JSXExpressionContainer') {
|
||||
// value could be a literal
|
||||
return;
|
||||
}
|
||||
|
||||
const valueExpression = valueNode.expression;
|
||||
const invocationScope = getScope(context, node);
|
||||
|
||||
// Check if the value prop is a construction
|
||||
const constructInfo = isConstruction(valueExpression, invocationScope);
|
||||
if (constructInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.getParentComponent(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report found error
|
||||
const constructType = constructInfo.type;
|
||||
const constructNode = constructInfo.node;
|
||||
const constructUsage = constructInfo.usage;
|
||||
const data = {
|
||||
type: constructType, nodeLine: constructNode.loc.start.line,
|
||||
};
|
||||
let messageId = 'defaultMsg';
|
||||
|
||||
// Variable passed to value prop
|
||||
if (constructUsage != null) {
|
||||
messageId = 'withIdentifierMsg';
|
||||
data.usageLine = constructUsage.loc.start.line;
|
||||
data.variableName = constructUsage.name;
|
||||
}
|
||||
|
||||
// Type of expression
|
||||
if (
|
||||
constructType === 'function expression'
|
||||
|| constructType === 'function declaration'
|
||||
) {
|
||||
messageId += 'Func';
|
||||
}
|
||||
|
||||
report(context, messages[messageId], messageId, {
|
||||
node: constructNode,
|
||||
data,
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
77
node_modules/eslint-plugin-react/lib/rules/jsx-no-duplicate-props.js
generated
vendored
Normal file
77
node_modules/eslint-plugin-react/lib/rules/jsx-no-duplicate-props.js
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @fileoverview Enforce no duplicate props
|
||||
* @author Markus Ånöstam
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noDuplicateProps: 'No duplicate props allowed',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow duplicate properties in JSX',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-no-duplicate-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreCase: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
const props = {};
|
||||
|
||||
node.attributes.forEach((decl) => {
|
||||
if (decl.type === 'JSXSpreadAttribute') {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = decl.name.name;
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
if (has(props, name)) {
|
||||
report(context, messages.noDuplicateProps, 'noDuplicateProps', {
|
||||
node: decl,
|
||||
});
|
||||
} else {
|
||||
props[name] = 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
204
node_modules/eslint-plugin-react/lib/rules/jsx-no-leaked-render.js
generated
vendored
Normal file
204
node_modules/eslint-plugin-react/lib/rules/jsx-no-leaked-render.js
generated
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* @fileoverview Prevent problematic leaked values from being rendered
|
||||
* @author Mario Beltrán
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const find = require('es-iterator-helpers/Iterator.prototype.find');
|
||||
const from = require('es-iterator-helpers/Iterator.from');
|
||||
|
||||
const getText = require('../util/eslint').getText;
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const variableUtil = require('../util/variable');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const isParenthesized = require('../util/ast').isParenthesized;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noPotentialLeakedRender: 'Potential leaked value that might cause unintentionally rendered values or rendering crashes',
|
||||
};
|
||||
|
||||
const COERCE_STRATEGY = 'coerce';
|
||||
const TERNARY_STRATEGY = 'ternary';
|
||||
const DEFAULT_VALID_STRATEGIES = [TERNARY_STRATEGY, COERCE_STRATEGY];
|
||||
const COERCE_VALID_LEFT_SIDE_EXPRESSIONS = ['UnaryExpression', 'BinaryExpression', 'CallExpression'];
|
||||
const TERNARY_INVALID_ALTERNATE_VALUES = [undefined, null, false];
|
||||
|
||||
function trimLeftNode(node) {
|
||||
// Remove double unary expression (boolean coercion), so we avoid trimming valid negations
|
||||
if (node.type === 'UnaryExpression' && node.argument.type === 'UnaryExpression') {
|
||||
return trimLeftNode(node.argument.argument);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function getIsCoerceValidNestedLogicalExpression(node) {
|
||||
if (node.type === 'LogicalExpression') {
|
||||
return getIsCoerceValidNestedLogicalExpression(node.left) && getIsCoerceValidNestedLogicalExpression(node.right);
|
||||
}
|
||||
|
||||
return COERCE_VALID_LEFT_SIDE_EXPRESSIONS.some((validExpression) => validExpression === node.type);
|
||||
}
|
||||
|
||||
function extractExpressionBetweenLogicalAnds(node) {
|
||||
if (node.type !== 'LogicalExpression') return [node];
|
||||
if (node.operator !== '&&') return [node];
|
||||
return [].concat(
|
||||
extractExpressionBetweenLogicalAnds(node.left),
|
||||
extractExpressionBetweenLogicalAnds(node.right)
|
||||
);
|
||||
}
|
||||
|
||||
function ruleFixer(context, fixStrategy, fixer, reportedNode, leftNode, rightNode) {
|
||||
const rightSideText = getText(context, rightNode);
|
||||
|
||||
if (fixStrategy === COERCE_STRATEGY) {
|
||||
const expressions = extractExpressionBetweenLogicalAnds(leftNode);
|
||||
const newText = expressions.map((node) => {
|
||||
let nodeText = getText(context, node);
|
||||
if (isParenthesized(context, node)) {
|
||||
nodeText = `(${nodeText})`;
|
||||
}
|
||||
if (node.parent && node.parent.type === 'ConditionalExpression' && node.parent.consequent.value === false) {
|
||||
return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!'}${nodeText}`;
|
||||
}
|
||||
return `${getIsCoerceValidNestedLogicalExpression(node) ? '' : '!!'}${nodeText}`;
|
||||
}).join(' && ');
|
||||
|
||||
if (rightNode.parent && rightNode.parent.type === 'ConditionalExpression' && rightNode.parent.consequent.value === false) {
|
||||
const consequentVal = rightNode.parent.consequent.raw || rightNode.parent.consequent.name;
|
||||
const alternateVal = rightNode.parent.alternate.raw || rightNode.parent.alternate.name;
|
||||
if (rightNode.parent.test && rightNode.parent.test.type === 'LogicalExpression') {
|
||||
return fixer.replaceText(reportedNode, `${newText} ? ${consequentVal} : ${alternateVal}`);
|
||||
}
|
||||
return fixer.replaceText(reportedNode, `${newText} && ${alternateVal}`);
|
||||
}
|
||||
|
||||
if (rightNode.type === 'ConditionalExpression' || rightNode.type === 'LogicalExpression') {
|
||||
return fixer.replaceText(reportedNode, `${newText} && (${rightSideText})`);
|
||||
}
|
||||
if (rightNode.type === 'JSXElement') {
|
||||
const rightSideTextLines = rightSideText.split('\n');
|
||||
if (rightSideTextLines.length > 1) {
|
||||
const rightSideTextLastLine = rightSideTextLines[rightSideTextLines.length - 1];
|
||||
const indentSpacesStart = ' '.repeat(rightSideTextLastLine.search(/\S/));
|
||||
const indentSpacesClose = ' '.repeat(rightSideTextLastLine.search(/\S/) - 2);
|
||||
return fixer.replaceText(reportedNode, `${newText} && (\n${indentSpacesStart}${rightSideText}\n${indentSpacesClose})`);
|
||||
}
|
||||
}
|
||||
if (rightNode.type === 'Literal') {
|
||||
return null;
|
||||
}
|
||||
return fixer.replaceText(reportedNode, `${newText} && ${rightSideText}`);
|
||||
}
|
||||
|
||||
if (fixStrategy === TERNARY_STRATEGY) {
|
||||
let leftSideText = getText(context, trimLeftNode(leftNode));
|
||||
if (isParenthesized(context, leftNode)) {
|
||||
leftSideText = `(${leftSideText})`;
|
||||
}
|
||||
return fixer.replaceText(reportedNode, `${leftSideText} ? ${rightSideText} : null`);
|
||||
}
|
||||
|
||||
throw new TypeError('Invalid value for "validStrategies" option');
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow problematic leaked values from being rendered',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-leaked-render'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
fixable: 'code',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
validStrategies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
enum: [
|
||||
TERNARY_STRATEGY,
|
||||
COERCE_STRATEGY,
|
||||
],
|
||||
},
|
||||
uniqueItems: true,
|
||||
default: DEFAULT_VALID_STRATEGIES,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const validStrategies = new Set(config.validStrategies || DEFAULT_VALID_STRATEGIES);
|
||||
const fixStrategy = find(from(validStrategies), () => true);
|
||||
|
||||
return {
|
||||
'JSXExpressionContainer > LogicalExpression[operator="&&"]'(node) {
|
||||
const leftSide = node.left;
|
||||
|
||||
const isCoerceValidLeftSide = COERCE_VALID_LEFT_SIDE_EXPRESSIONS
|
||||
.some((validExpression) => validExpression === leftSide.type);
|
||||
if (validStrategies.has(COERCE_STRATEGY)) {
|
||||
if (isCoerceValidLeftSide || getIsCoerceValidNestedLogicalExpression(leftSide)) {
|
||||
return;
|
||||
}
|
||||
const leftSideVar = variableUtil.getVariableFromContext(context, node, leftSide.name);
|
||||
if (leftSideVar) {
|
||||
const leftSideValue = leftSideVar.defs
|
||||
&& leftSideVar.defs.length
|
||||
&& leftSideVar.defs[0].node.init
|
||||
&& leftSideVar.defs[0].node.init.value;
|
||||
if (typeof leftSideValue === 'boolean') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (testReactVersion(context, '>= 18') && leftSide.type === 'Literal' && leftSide.value === '') {
|
||||
return;
|
||||
}
|
||||
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
|
||||
node,
|
||||
fix(fixer) {
|
||||
return ruleFixer(context, fixStrategy, fixer, node, leftSide, node.right);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
'JSXExpressionContainer > ConditionalExpression'(node) {
|
||||
if (validStrategies.has(TERNARY_STRATEGY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidTernaryAlternate = TERNARY_INVALID_ALTERNATE_VALUES.indexOf(node.alternate.value) === -1;
|
||||
const isJSXElementAlternate = node.alternate.type === 'JSXElement';
|
||||
if (isValidTernaryAlternate || isJSXElementAlternate) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.noPotentialLeakedRender, 'noPotentialLeakedRender', {
|
||||
node,
|
||||
fix(fixer) {
|
||||
return ruleFixer(context, fixStrategy, fixer, node, node.test, node.consequent);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
555
node_modules/eslint-plugin-react/lib/rules/jsx-no-literals.js
generated
vendored
Normal file
555
node_modules/eslint-plugin-react/lib/rules/jsx-no-literals.js
generated
vendored
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* @fileoverview Prevent using string literals in React component definition
|
||||
* @author Caleb Morris
|
||||
* @author David Buchan-Swanson
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const iterFrom = require('es-iterator-helpers/Iterator.from');
|
||||
const map = require('es-iterator-helpers/Iterator.prototype.map');
|
||||
const some = require('es-iterator-helpers/Iterator.prototype.some');
|
||||
const flatMap = require('es-iterator-helpers/Iterator.prototype.flatMap');
|
||||
const fromEntries = require('object.fromentries');
|
||||
const entries = require('object.entries');
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {string | unknown}
|
||||
*/
|
||||
function trimIfString(value) {
|
||||
return typeof value === 'string' ? value.trim() : value;
|
||||
}
|
||||
|
||||
const reOverridableElement = /^[A-Z][\w.]*$/;
|
||||
const reIsWhiteSpace = /^[\s]+$/;
|
||||
const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']);
|
||||
const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']);
|
||||
|
||||
const messages = {
|
||||
invalidPropValue: 'Invalid prop value: "{{text}}"',
|
||||
invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}',
|
||||
noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
|
||||
noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}',
|
||||
noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
|
||||
noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}',
|
||||
literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
|
||||
literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}',
|
||||
};
|
||||
|
||||
/** @type {Exclude<import('eslint').Rule.RuleModule['meta']['schema'], unknown[]>['properties']} */
|
||||
const commonPropertiesSchema = {
|
||||
noStrings: {
|
||||
type: 'boolean',
|
||||
},
|
||||
allowedStrings: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
ignoreProps: {
|
||||
type: 'boolean',
|
||||
},
|
||||
noAttributeStrings: {
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef RawElementConfigProperties
|
||||
* @property {boolean} [noStrings]
|
||||
* @property {string[]} [allowedStrings]
|
||||
* @property {boolean} [ignoreProps]
|
||||
* @property {boolean} [noAttributeStrings]
|
||||
*
|
||||
* @typedef RawOverrideConfigProperties
|
||||
* @property {boolean} [allowElement]
|
||||
* @property {boolean} [applyToNestedElements=true]
|
||||
*
|
||||
* @typedef {RawElementConfigProperties} RawElementConfig
|
||||
* @typedef {RawElementConfigProperties & RawElementConfigProperties} RawOverrideConfig
|
||||
*
|
||||
* @typedef RawElementOverrides
|
||||
* @property {Record<string, RawOverrideConfig>} [elementOverrides]
|
||||
*
|
||||
* @typedef {RawElementConfig & RawElementOverrides} RawConfig
|
||||
*
|
||||
* ----------------------------------------------------------------------
|
||||
*
|
||||
* @typedef ElementConfigType
|
||||
* @property {'element'} type
|
||||
*
|
||||
* @typedef ElementConfigProperties
|
||||
* @property {boolean} noStrings
|
||||
* @property {Set<string>} allowedStrings
|
||||
* @property {boolean} ignoreProps
|
||||
* @property {boolean} noAttributeStrings
|
||||
*
|
||||
* @typedef OverrideConfigProperties
|
||||
* @property {'override'} type
|
||||
* @property {string} name
|
||||
* @property {boolean} allowElement
|
||||
* @property {boolean} applyToNestedElements
|
||||
*
|
||||
* @typedef {ElementConfigType & ElementConfigProperties} ElementConfig
|
||||
* @typedef {OverrideConfigProperties & ElementConfigProperties} OverrideConfig
|
||||
*
|
||||
* @typedef ElementOverrides
|
||||
* @property {Record<string, OverrideConfig>} elementOverrides
|
||||
*
|
||||
* @typedef {ElementConfig & ElementOverrides} Config
|
||||
* @typedef {Config | OverrideConfig} ResolvedConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes the element portion of the config
|
||||
* @param {RawConfig} config
|
||||
* @returns {ElementConfig}
|
||||
*/
|
||||
function normalizeElementConfig(config) {
|
||||
return {
|
||||
type: 'element',
|
||||
noStrings: !!config.noStrings,
|
||||
allowedStrings: config.allowedStrings
|
||||
? new Set(map(iterFrom(config.allowedStrings), trimIfString))
|
||||
: new Set(),
|
||||
ignoreProps: !!config.ignoreProps,
|
||||
noAttributeStrings: !!config.noAttributeStrings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the config and applies default values to all config options
|
||||
* @param {RawConfig} config
|
||||
* @returns {Config}
|
||||
*/
|
||||
function normalizeConfig(config) {
|
||||
/** @type {Config} */
|
||||
const normalizedConfig = Object.assign(normalizeElementConfig(config), {
|
||||
elementOverrides: {},
|
||||
});
|
||||
|
||||
if (config.elementOverrides) {
|
||||
normalizedConfig.elementOverrides = fromEntries(
|
||||
flatMap(
|
||||
iterFrom(entries(config.elementOverrides)),
|
||||
(entry) => {
|
||||
const elementName = entry[0];
|
||||
const rawElementConfig = entry[1];
|
||||
|
||||
if (!reOverridableElement.test(elementName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
elementName,
|
||||
Object.assign(normalizeElementConfig(rawElementConfig), {
|
||||
type: 'override',
|
||||
name: elementName,
|
||||
allowElement: !!rawElementConfig.allowElement,
|
||||
applyToNestedElements: typeof rawElementConfig.applyToNestedElements === 'undefined' || !!rawElementConfig.applyToNestedElements,
|
||||
}),
|
||||
]];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedConfig;
|
||||
}
|
||||
|
||||
const elementOverrides = {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
[reOverridableElement.source]: {
|
||||
type: 'object',
|
||||
properties: Object.assign(
|
||||
{ applyToNestedElements: { type: 'boolean' } },
|
||||
commonPropertiesSchema
|
||||
),
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of string literals in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-literals'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: Object.assign(
|
||||
{ elementOverrides },
|
||||
commonPropertiesSchema
|
||||
),
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
/** @type {RawConfig} */
|
||||
const rawConfig = (context.options.length && context.options[0]) || {};
|
||||
const config = normalizeConfig(rawConfig);
|
||||
|
||||
const hasElementOverrides = Object.keys(config.elementOverrides).length > 0;
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
const renamedImportMap = new Map();
|
||||
|
||||
/**
|
||||
* Determines if the given expression is a require statement. Supports
|
||||
* nested MemberExpresions. ie `require('foo').nested.property`
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isRequireStatement(node) {
|
||||
if (node.type === 'CallExpression') {
|
||||
if (node.callee.type === 'Identifier') {
|
||||
return node.callee.name === 'require';
|
||||
}
|
||||
}
|
||||
if (node.type === 'MemberExpression') {
|
||||
return isRequireStatement(node.object);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @typedef {{ name: string, compoundName?: string }} ElementNameFragment */
|
||||
|
||||
/**
|
||||
* Gets the name of the given JSX element. Supports nested
|
||||
* JSXMemeberExpressions. ie `<Namesapce.Component.SubComponent />`
|
||||
* @param {ASTNode} node
|
||||
* @returns {ElementNameFragment | undefined}
|
||||
*/
|
||||
function getJSXElementName(node) {
|
||||
if (node.openingElement.name.type === 'JSXIdentifier') {
|
||||
const name = node.openingElement.name.name;
|
||||
return {
|
||||
name: renamedImportMap.get(name) || name,
|
||||
compoundName: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const nameFragments = [];
|
||||
|
||||
if (node.openingElement.name.type === 'JSXMemberExpression') {
|
||||
/** @type {ASTNode} */
|
||||
let current = node.openingElement.name;
|
||||
while (current.type === 'JSXMemberExpression') {
|
||||
if (current.property.type === 'JSXIdentifier') {
|
||||
nameFragments.unshift(current.property.name);
|
||||
}
|
||||
|
||||
current = current.object;
|
||||
}
|
||||
|
||||
if (current.type === 'JSXIdentifier') {
|
||||
nameFragments.unshift(current.name);
|
||||
|
||||
const rootFragment = nameFragments[0];
|
||||
if (rootFragment) {
|
||||
const rootFragmentRenamed = renamedImportMap.get(rootFragment);
|
||||
if (rootFragmentRenamed) {
|
||||
nameFragments[0] = rootFragmentRenamed;
|
||||
}
|
||||
}
|
||||
|
||||
const nameFragment = nameFragments[nameFragments.length - 1];
|
||||
if (nameFragment) {
|
||||
return {
|
||||
name: nameFragment,
|
||||
compoundName: nameFragments.join('.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all JSXElement ancestor nodes for the given node
|
||||
* @param {ASTNode} node
|
||||
* @returns {ASTNode[]}
|
||||
*/
|
||||
function getJSXElementAncestors(node) {
|
||||
/** @type {ASTNode[]} */
|
||||
const ancestors = [];
|
||||
|
||||
let current = node;
|
||||
while (current) {
|
||||
if (current.type === 'JSXElement') {
|
||||
ancestors.push(current);
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {ASTNode}
|
||||
*/
|
||||
function getParentIgnoringBinaryExpressions(node) {
|
||||
let current = node;
|
||||
while (current.parent.type === 'BinaryExpression') {
|
||||
current = current.parent;
|
||||
}
|
||||
return current.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {{ parent: ASTNode, grandParent: ASTNode }}
|
||||
*/
|
||||
function getParentAndGrandParent(node) {
|
||||
const parent = getParentIgnoringBinaryExpressions(node);
|
||||
return {
|
||||
parent,
|
||||
grandParent: parent.parent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasJSXElementParentOrGrandParent(node) {
|
||||
const ancestors = getParentAndGrandParent(node);
|
||||
return some(iterFrom([ancestors.parent, ancestors.grandParent]), (parent) => jsxElementTypes.has(parent.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given node's value and its immediate parent are
|
||||
* viable text nodes that can/should be reported on
|
||||
* @param {ASTNode} node
|
||||
* @param {ResolvedConfig} resolvedConfig
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isViableTextNode(node, resolvedConfig) {
|
||||
const textValues = iterFrom([trimIfString(node.raw), trimIfString(node.value)]);
|
||||
if (some(textValues, (value) => resolvedConfig.allowedStrings.has(value))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent = getParentIgnoringBinaryExpressions(node);
|
||||
|
||||
let isStandardJSXNode = false;
|
||||
if (typeof node.value === 'string' && !reIsWhiteSpace.test(node.value) && standardJSXNodeParentTypes.has(parent.type)) {
|
||||
if (resolvedConfig.noAttributeStrings) {
|
||||
isStandardJSXNode = parent.type === 'JSXAttribute' || parent.type === 'JSXElement';
|
||||
} else {
|
||||
isStandardJSXNode = parent.type !== 'JSXAttribute';
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedConfig.noStrings) {
|
||||
return isStandardJSXNode;
|
||||
}
|
||||
|
||||
return isStandardJSXNode && parent.type !== 'JSXExpressionContainer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an override config for a given node. For any given node, we also
|
||||
* need to traverse the ancestor tree to determine if an ancestor's config
|
||||
* will also apply to the current node.
|
||||
* @param {ASTNode} node
|
||||
* @returns {OverrideConfig | undefined}
|
||||
*/
|
||||
function getOverrideConfig(node) {
|
||||
if (!hasElementOverrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allAncestorElements = getJSXElementAncestors(node);
|
||||
if (!allAncestorElements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ancestorElement of allAncestorElements) {
|
||||
const isClosestJSXAncestor = ancestorElement === allAncestorElements[0];
|
||||
|
||||
const ancestor = getJSXElementName(ancestorElement);
|
||||
if (ancestor) {
|
||||
if (ancestor.name) {
|
||||
const ancestorElements = config.elementOverrides[ancestor.name];
|
||||
const ancestorConfig = ancestor.compoundName
|
||||
? config.elementOverrides[ancestor.compoundName] || ancestorElements
|
||||
: ancestorElements;
|
||||
|
||||
if (ancestorConfig) {
|
||||
if (isClosestJSXAncestor || ancestorConfig.applyToNestedElements) {
|
||||
return ancestorConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolvedConfig} resolvedConfig
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldAllowElement(resolvedConfig) {
|
||||
return resolvedConfig.type === 'override' && 'allowElement' in resolvedConfig && !!resolvedConfig.allowElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} ancestorIsJSXElement
|
||||
* @param {ResolvedConfig} resolvedConfig
|
||||
* @returns {string}
|
||||
*/
|
||||
function defaultMessageId(ancestorIsJSXElement, resolvedConfig) {
|
||||
if (resolvedConfig.noAttributeStrings && !ancestorIsJSXElement) {
|
||||
return resolvedConfig.type === 'override' ? 'noStringsInAttributesInElement' : 'noStringsInAttributes';
|
||||
}
|
||||
|
||||
if (resolvedConfig.noStrings) {
|
||||
return resolvedConfig.type === 'override' ? 'noStringsInJSXInElement' : 'noStringsInJSX';
|
||||
}
|
||||
|
||||
return resolvedConfig.type === 'override' ? 'literalNotInJSXExpressionInElement' : 'literalNotInJSXExpression';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @param {string} messageId
|
||||
* @param {ResolvedConfig} resolvedConfig
|
||||
*/
|
||||
function reportLiteralNode(node, messageId, resolvedConfig) {
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
data: {
|
||||
text: getText(context, node).trim(),
|
||||
element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return Object.assign(hasElementOverrides ? {
|
||||
// Get renamed import local names mapped to their imported name
|
||||
ImportDeclaration(node) {
|
||||
node.specifiers
|
||||
.filter((s) => s.type === 'ImportSpecifier')
|
||||
.forEach((specifier) => {
|
||||
renamedImportMap.set(
|
||||
(specifier.local || specifier.imported).name,
|
||||
specifier.imported.name
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
// Get renamed destructured local names mapped to their imported name
|
||||
VariableDeclaration(node) {
|
||||
node.declarations
|
||||
.filter((d) => (
|
||||
d.type === 'VariableDeclarator'
|
||||
&& isRequireStatement(d.init)
|
||||
&& d.id.type === 'ObjectPattern'
|
||||
))
|
||||
.forEach((declaration) => {
|
||||
declaration.id.properties
|
||||
.filter((property) => (
|
||||
property.type === 'Property'
|
||||
&& property.key.type === 'Identifier'
|
||||
&& property.value.type === 'Identifier'
|
||||
))
|
||||
.forEach((property) => {
|
||||
renamedImportMap.set(property.value.name, property.key.name);
|
||||
});
|
||||
});
|
||||
},
|
||||
} : false, {
|
||||
Literal(node) {
|
||||
const resolvedConfig = getOverrideConfig(node) || config;
|
||||
|
||||
const hasJSXParentOrGrandParent = hasJSXElementParentOrGrandParent(node);
|
||||
if (hasJSXParentOrGrandParent && shouldAllowElement(resolvedConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isViableTextNode(node, resolvedConfig)) {
|
||||
if (hasJSXParentOrGrandParent || !config.ignoreProps) {
|
||||
reportLiteralNode(node, defaultMessageId(hasJSXParentOrGrandParent, resolvedConfig), resolvedConfig);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
const isLiteralString = node.value && node.value.type === 'Literal'
|
||||
&& typeof node.value.value === 'string';
|
||||
const isStringLiteral = node.value && node.value.type === 'StringLiteral';
|
||||
|
||||
if (isLiteralString || isStringLiteral) {
|
||||
const resolvedConfig = getOverrideConfig(node) || config;
|
||||
|
||||
if (
|
||||
resolvedConfig.noStrings
|
||||
&& !resolvedConfig.ignoreProps
|
||||
&& !resolvedConfig.allowedStrings.has(node.value.value)
|
||||
) {
|
||||
const messageId = resolvedConfig.type === 'override' ? 'invalidPropValueInElement' : 'invalidPropValue';
|
||||
reportLiteralNode(node, messageId, resolvedConfig);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
JSXText(node) {
|
||||
const resolvedConfig = getOverrideConfig(node) || config;
|
||||
|
||||
if (shouldAllowElement(resolvedConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isViableTextNode(node, resolvedConfig)) {
|
||||
const hasJSXParendOrGrantParent = hasJSXElementParentOrGrandParent(node);
|
||||
reportLiteralNode(node, defaultMessageId(hasJSXParendOrGrantParent, resolvedConfig), resolvedConfig);
|
||||
}
|
||||
},
|
||||
|
||||
TemplateLiteral(node) {
|
||||
const ancestors = getParentAndGrandParent(node);
|
||||
const isParentJSXExpressionCont = ancestors.parent.type === 'JSXExpressionContainer';
|
||||
const isParentJSXElement = ancestors.grandParent.type === 'JSXElement';
|
||||
|
||||
if (isParentJSXExpressionCont) {
|
||||
const resolvedConfig = getOverrideConfig(node) || config;
|
||||
|
||||
if (
|
||||
resolvedConfig.noStrings
|
||||
&& (isParentJSXElement || !resolvedConfig.ignoreProps)
|
||||
) {
|
||||
reportLiteralNode(node, defaultMessageId(isParentJSXElement, resolvedConfig), resolvedConfig);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
143
node_modules/eslint-plugin-react/lib/rules/jsx-no-script-url.js
generated
vendored
Normal file
143
node_modules/eslint-plugin-react/lib/rules/jsx-no-script-url.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of `javascript:` URLs
|
||||
* @author Sergei Startsev
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const includes = require('array-includes');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const linkComponentsUtil = require('../util/linkComponents');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
// https://github.com/facebook/react/blob/d0ebde77f6d1232cefc0da184d731943d78e86f2/packages/react-dom/src/shared/sanitizeURL.js#L30
|
||||
/* eslint-disable-next-line max-len, no-control-regex */
|
||||
const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
||||
|
||||
function hasJavaScriptProtocol(attr) {
|
||||
return attr.value && attr.value.type === 'Literal'
|
||||
&& isJavaScriptProtocol.test(attr.value.value);
|
||||
}
|
||||
|
||||
function shouldVerifyProp(node, config) {
|
||||
const name = node.name && node.name.name;
|
||||
const parentName = node.parent.name && node.parent.name.name;
|
||||
|
||||
if (!name || !parentName || !config.has(parentName)) return false;
|
||||
|
||||
const attributes = config.get(parentName);
|
||||
return includes(attributes, name);
|
||||
}
|
||||
|
||||
function parseLegacyOption(config, option) {
|
||||
option.forEach((opt) => {
|
||||
config.set(opt.name, opt.props);
|
||||
});
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noScriptURL: 'A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of `javascript:` URLs',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-script-url'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
props: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['name', 'props'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeFromSettings: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalItems: false,
|
||||
},
|
||||
],
|
||||
additionalItems: false,
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
includeFromSettings: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalItems: false,
|
||||
},
|
||||
],
|
||||
additionalItems: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options;
|
||||
const hasLegacyOption = Array.isArray(options[0]);
|
||||
const legacyOptions = hasLegacyOption ? options[0] : [];
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const objectOption = (hasLegacyOption && options.length > 1)
|
||||
? options[1]
|
||||
: (options.length > 0
|
||||
? options[0]
|
||||
: {
|
||||
includeFromSettings: false,
|
||||
}
|
||||
);
|
||||
const includeFromSettings = objectOption.includeFromSettings;
|
||||
|
||||
const linkComponents = linkComponentsUtil.getLinkComponents(includeFromSettings ? context : {});
|
||||
parseLegacyOption(linkComponents, legacyOptions);
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (shouldVerifyProp(node, linkComponents) && hasJavaScriptProtocol(node)) {
|
||||
report(context, messages.noScriptURL, 'noScriptURL', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
285
node_modules/eslint-plugin-react/lib/rules/jsx-no-target-blank.js
generated
vendored
Normal file
285
node_modules/eslint-plugin-react/lib/rules/jsx-no-target-blank.js
generated
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @fileoverview Forbid target='_blank' attribute
|
||||
* @author Kevin Miller
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const includes = require('array-includes');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const linkComponentsUtil = require('../util/linkComponents');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function findLastIndex(arr, condition) {
|
||||
for (let i = arr.length - 1; i >= 0; i -= 1) {
|
||||
if (condition(arr[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function attributeValuePossiblyBlank(attribute) {
|
||||
if (!attribute || !attribute.value) {
|
||||
return false;
|
||||
}
|
||||
const value = attribute.value;
|
||||
if (value.type === 'Literal') {
|
||||
return typeof value.value === 'string' && value.value.toLowerCase() === '_blank';
|
||||
}
|
||||
if (value.type === 'JSXExpressionContainer') {
|
||||
const expr = value.expression;
|
||||
if (expr.type === 'Literal') {
|
||||
return typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank';
|
||||
}
|
||||
if (expr.type === 'ConditionalExpression') {
|
||||
if (expr.alternate.type === 'Literal' && expr.alternate.value && expr.alternate.value.toLowerCase() === '_blank') {
|
||||
return true;
|
||||
}
|
||||
if (expr.consequent.type === 'Literal' && expr.consequent.value && expr.consequent.value.toLowerCase() === '_blank') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex) {
|
||||
const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && includes(linkAttributes, attr.name.name));
|
||||
const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value && attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))(
|
||||
node.attributes[linkIndex]);
|
||||
return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex);
|
||||
}
|
||||
|
||||
function hasDynamicLink(node, linkAttributes) {
|
||||
const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name
|
||||
&& includes(linkAttributes, attr.name.name)
|
||||
&& attr.value
|
||||
&& attr.value.type === 'JSXExpressionContainer');
|
||||
if (dynamicLinkIndex !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string(s) from a value
|
||||
* @param {ASTNode} value The AST node being checked.
|
||||
* @param {ASTNode} targetValue The AST node being checked.
|
||||
* @returns {string | string[] | null} The string value, or null if not a string.
|
||||
*/
|
||||
function getStringFromValue(value, targetValue) {
|
||||
if (value) {
|
||||
if (value.type === 'Literal') {
|
||||
return value.value;
|
||||
}
|
||||
if (value.type === 'JSXExpressionContainer') {
|
||||
if (value.expression.type === 'TemplateLiteral') {
|
||||
return value.expression.quasis[0].value.cooked;
|
||||
}
|
||||
const expr = value.expression;
|
||||
if (expr && expr.type === 'ConditionalExpression') {
|
||||
const relValues = [expr.consequent.value, expr.alternate.value];
|
||||
if (targetValue.type === 'JSXExpressionContainer' && targetValue.expression && targetValue.expression.type === 'ConditionalExpression') {
|
||||
const targetTestCond = targetValue.expression.test.name;
|
||||
const relTestCond = value.expression.test.name;
|
||||
if (targetTestCond === relTestCond) {
|
||||
const targetBlankIndex = [targetValue.expression.consequent.value, targetValue.expression.alternate.value].indexOf('_blank');
|
||||
return relValues[targetBlankIndex];
|
||||
}
|
||||
}
|
||||
return relValues;
|
||||
}
|
||||
return expr.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) {
|
||||
const relIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel'));
|
||||
const targetIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'target'));
|
||||
if (relIndex === -1 || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relAttribute = node.attributes[relIndex];
|
||||
const targetAttributeValue = node.attributes[targetIndex] && node.attributes[targetIndex].value;
|
||||
const value = getStringFromValue(relAttribute.value, targetAttributeValue);
|
||||
return [].concat(value).every((item) => {
|
||||
const tags = typeof item === 'string' ? item.toLowerCase().split(' ') : false;
|
||||
const noreferrer = tags && tags.indexOf('noreferrer') >= 0;
|
||||
if (noreferrer) {
|
||||
return true;
|
||||
}
|
||||
const noopener = tags && tags.indexOf('noopener') >= 0;
|
||||
return allowReferrer && noopener;
|
||||
});
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noTargetBlankWithoutNoreferrer: 'Using target="_blank" without rel="noreferrer" (which implies rel="noopener") is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations',
|
||||
noTargetBlankWithoutNoopener: 'Using target="_blank" without rel="noreferrer" or rel="noopener" (the former implies the latter and is preferred due to wider support) is a security risk: see https://mathiasbynens.github.io/rel-noopener/#recommendations',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
docs: {
|
||||
description: 'Disallow `target="_blank"` attribute without `rel="noreferrer"`',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-no-target-blank'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowReferrer: {
|
||||
type: 'boolean',
|
||||
},
|
||||
enforceDynamicLinks: {
|
||||
enum: ['always', 'never'],
|
||||
},
|
||||
warnOnSpreadAttributes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
links: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
forms: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = Object.assign(
|
||||
{
|
||||
allowReferrer: false,
|
||||
warnOnSpreadAttributes: false,
|
||||
links: true,
|
||||
forms: false,
|
||||
},
|
||||
context.options[0]
|
||||
);
|
||||
const allowReferrer = configuration.allowReferrer;
|
||||
const warnOnSpreadAttributes = configuration.warnOnSpreadAttributes;
|
||||
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
|
||||
const linkComponents = linkComponentsUtil.getLinkComponents(context);
|
||||
const formComponents = linkComponentsUtil.getFormComponents(context);
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
const targetIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === 'target');
|
||||
const spreadAttributeIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute'));
|
||||
|
||||
if (linkComponents.has(node.name.name)) {
|
||||
if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) {
|
||||
const hasSpread = spreadAttributeIndex >= 0;
|
||||
|
||||
if (warnOnSpreadAttributes && hasSpread) {
|
||||
// continue to check below
|
||||
} else if ((hasSpread && targetIndex < spreadAttributeIndex) || !hasSpread || !warnOnSpreadAttributes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const linkAttributes = linkComponents.get(node.name.name);
|
||||
const hasDangerousLink = hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex)
|
||||
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttributes));
|
||||
if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) {
|
||||
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
|
||||
const relValue = allowReferrer ? 'noopener' : 'noreferrer';
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
fix(fixer) {
|
||||
// eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes`
|
||||
const nodeWithAttrs = node.parent.attributes ? node.parent : node;
|
||||
// eslint 5 does not provide a `name` property on JSXSpreadElements
|
||||
const relAttribute = nodeWithAttrs.attributes.find((attr) => attr.name && attr.name.name === 'rel');
|
||||
|
||||
if (targetIndex < spreadAttributeIndex || (spreadAttributeIndex >= 0 && !relAttribute)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!relAttribute) {
|
||||
return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ` rel="${relValue}"`);
|
||||
}
|
||||
|
||||
if (!relAttribute.value) {
|
||||
return fixer.insertTextAfter(relAttribute, `="${relValue}"`);
|
||||
}
|
||||
|
||||
if (relAttribute.value.type === 'Literal') {
|
||||
const parts = relAttribute.value.value
|
||||
.split('noreferrer')
|
||||
.filter(Boolean);
|
||||
return fixer.replaceText(relAttribute.value, `"${parts.concat('noreferrer').join(' ')}"`);
|
||||
}
|
||||
|
||||
if (relAttribute.value.type === 'JSXExpressionContainer') {
|
||||
if (relAttribute.value.expression.type === 'Literal') {
|
||||
if (typeof relAttribute.value.expression.value === 'string') {
|
||||
const parts = relAttribute.value.expression.value
|
||||
.split('noreferrer')
|
||||
.filter(Boolean);
|
||||
return fixer.replaceText(relAttribute.value.expression, `"${parts.concat('noreferrer').join(' ')}"`);
|
||||
}
|
||||
|
||||
// for undefined, boolean, number, symbol, bigint, and null
|
||||
return fixer.replaceText(relAttribute.value, '"noreferrer"');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (formComponents.has(node.name.name)) {
|
||||
if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) {
|
||||
const hasSpread = spreadAttributeIndex >= 0;
|
||||
|
||||
if (warnOnSpreadAttributes && hasSpread) {
|
||||
// continue to check below
|
||||
} else if (
|
||||
(hasSpread && targetIndex < spreadAttributeIndex)
|
||||
|| !hasSpread
|
||||
|| !warnOnSpreadAttributes
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configuration.forms || hasSecureRel(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formAttributes = formComponents.get(node.name.name);
|
||||
|
||||
if (
|
||||
hasExternalLink(node, formAttributes)
|
||||
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttributes))
|
||||
) {
|
||||
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
117
node_modules/eslint-plugin-react/lib/rules/jsx-no-undef.js
generated
vendored
Normal file
117
node_modules/eslint-plugin-react/lib/rules/jsx-no-undef.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Disallow undeclared variables in JSX
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
undefined: '\'{{identifier}}\' is not defined.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow undeclared variables in JSX',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-no-undef'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowGlobals: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const allowGlobals = config.allowGlobals || false;
|
||||
|
||||
/**
|
||||
* Compare an identifier with the variables declared in the scope
|
||||
* @param {ASTNode} node - Identifier or JSXIdentifier node
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkIdentifierInJSX(node) {
|
||||
let scope = eslintUtil.getScope(context, node);
|
||||
const sourceCode = eslintUtil.getSourceCode(context);
|
||||
const sourceType = sourceCode.ast.sourceType;
|
||||
const scopeUpperBound = !allowGlobals && sourceType === 'module' ? 'module' : 'global';
|
||||
let variables = scope.variables;
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX)
|
||||
if (node.name === 'this') {
|
||||
return;
|
||||
}
|
||||
|
||||
while (scope.type !== scopeUpperBound && scope.type !== 'global') {
|
||||
scope = scope.upper;
|
||||
variables = scope.variables.concat(variables);
|
||||
}
|
||||
if (scope.childScopes.length) {
|
||||
variables = scope.childScopes[0].variables.concat(variables);
|
||||
// Temporary fix for babel-eslint
|
||||
if (scope.childScopes[0].childScopes.length) {
|
||||
variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, len = variables.length; i < len; i++) {
|
||||
if (variables[i].name === node.name) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
report(context, messages.undefined, 'undefined', {
|
||||
node,
|
||||
data: {
|
||||
identifier: node.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
switch (node.name.type) {
|
||||
case 'JSXIdentifier':
|
||||
if (jsxUtil.isDOMComponent(node)) {
|
||||
return;
|
||||
}
|
||||
node = node.name;
|
||||
break;
|
||||
case 'JSXMemberExpression':
|
||||
node = node.name;
|
||||
do {
|
||||
node = node.object;
|
||||
} while (node && node.type !== 'JSXIdentifier');
|
||||
break;
|
||||
case 'JSXNamespacedName':
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
checkIdentifierInJSX(node);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
259
node_modules/eslint-plugin-react/lib/rules/jsx-no-useless-fragment.js
generated
vendored
Normal file
259
node_modules/eslint-plugin-react/lib/rules/jsx-no-useless-fragment.js
generated
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @fileoverview Disallow useless fragments
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const arrayIncludes = require('array-includes');
|
||||
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const astUtil = require('../util/ast');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
function isJSXText(node) {
|
||||
return !!node && (node.type === 'JSXText' || node.type === 'Literal');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isOnlyWhitespace(text) {
|
||||
return text.trim().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isNonspaceJSXTextOrJSXCurly(node) {
|
||||
return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isFragmentWithOnlyTextAndIsNotChild(node) {
|
||||
return node.children.length === 1
|
||||
&& isJSXText(node.children[0])
|
||||
&& !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
function trimLikeReact(text) {
|
||||
const leadingSpaces = /^\s*/.exec(text)[0];
|
||||
const trailingSpaces = /\s*$/.exec(text)[0];
|
||||
|
||||
const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
|
||||
const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
|
||||
|
||||
return text.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if node is like `<Fragment key={_}>_</Fragment>`
|
||||
* @param {JSXElement} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isKeyedElement(node) {
|
||||
return node.type === 'JSXElement'
|
||||
&& node.openingElement.attributes
|
||||
&& node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function containsCallExpression(node) {
|
||||
return node
|
||||
&& node.type === 'JSXExpressionContainer'
|
||||
&& astUtil.isCallExpression(node.expression);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all.',
|
||||
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
fixable: 'code',
|
||||
docs: {
|
||||
description: 'Disallow unnecessary fragments',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-no-useless-fragment'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowExpressions: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const allowExpressions = config.allowExpressions || false;
|
||||
|
||||
const reactPragma = pragmaUtil.getFromContext(context);
|
||||
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
|
||||
|
||||
/**
|
||||
* Test whether a node is an padding spaces trimmed by react runtime.
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPaddingSpaces(node) {
|
||||
return isJSXText(node)
|
||||
&& isOnlyWhitespace(node.raw)
|
||||
&& arrayIncludes(node.raw, '\n');
|
||||
}
|
||||
|
||||
function isFragmentWithSingleExpression(node) {
|
||||
const children = node && node.children.filter((child) => !isPaddingSpaces(child));
|
||||
return (
|
||||
children
|
||||
&& children.length === 1
|
||||
&& children[0].type === 'JSXExpressionContainer'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a JSXElement has less than two children, excluding paddings spaces.
|
||||
* @param {JSXElement|JSXFragment} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasLessThanTwoChildren(node) {
|
||||
if (!node || !node.children) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @type {ASTNode[]} */
|
||||
const nonPaddingChildren = node.children.filter(
|
||||
(child) => !isPaddingSpaces(child)
|
||||
);
|
||||
|
||||
if (nonPaddingChildren.length < 2) {
|
||||
return !containsCallExpression(nonPaddingChildren[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JSXElement|JSXFragment} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isChildOfHtmlElement(node) {
|
||||
return node.parent.type === 'JSXElement'
|
||||
&& node.parent.openingElement.name.type === 'JSXIdentifier'
|
||||
&& /^[a-z]+$/.test(node.parent.openingElement.name.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JSXElement|JSXFragment} node
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isChildOfComponentElement(node) {
|
||||
return node.parent.type === 'JSXElement'
|
||||
&& !isChildOfHtmlElement(node)
|
||||
&& !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function canFix(node) {
|
||||
// Not safe to fix fragments without a jsx parent.
|
||||
if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) {
|
||||
// const a = <></>
|
||||
if (node.children.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// const a = <>cat {meow}</>
|
||||
if (node.children.some(isNonspaceJSXTextOrJSXCurly)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Not safe to fix `<Eeee><>foo</></Eeee>` because `Eeee` might require its children be a ReactElement.
|
||||
if (isChildOfComponentElement(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// old TS parser can't handle this one
|
||||
if (node.type === 'JSXFragment' && (!node.openingFragment || !node.closingFragment)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {Function | undefined}
|
||||
*/
|
||||
function getFix(node) {
|
||||
if (!canFix(node)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function fix(fixer) {
|
||||
const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement;
|
||||
const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement;
|
||||
|
||||
const childrenText = opener.selfClosing ? '' : getText(context).slice(opener.range[1], closer.range[0]);
|
||||
|
||||
return fixer.replaceText(node, trimLikeReact(childrenText));
|
||||
};
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (isKeyedElement(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
hasLessThanTwoChildren(node)
|
||||
&& !isFragmentWithOnlyTextAndIsNotChild(node)
|
||||
&& !(allowExpressions && isFragmentWithSingleExpression(node))
|
||||
) {
|
||||
report(context, messages.NeedsMoreChildren, 'NeedsMoreChildren', {
|
||||
node,
|
||||
fix: getFix(node),
|
||||
});
|
||||
}
|
||||
|
||||
if (isChildOfHtmlElement(node)) {
|
||||
report(context, messages.ChildOfHtmlElement, 'ChildOfHtmlElement', {
|
||||
node,
|
||||
fix: getFix(node),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
|
||||
checkNode(node);
|
||||
}
|
||||
},
|
||||
JSXFragment: checkNode,
|
||||
};
|
||||
},
|
||||
};
|
||||
249
node_modules/eslint-plugin-react/lib/rules/jsx-one-expression-per-line.js
generated
vendored
Normal file
249
node_modules/eslint-plugin-react/lib/rules/jsx-one-expression-per-line.js
generated
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* @fileoverview Limit to one expression per line in JSX
|
||||
* @author Mark Ivan Allen <Vydia.com>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const optionDefaults = {
|
||||
allow: 'none',
|
||||
};
|
||||
|
||||
const messages = {
|
||||
moveToNewLine: '`{{descriptor}}` must be placed on a new line',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Require one JSX element per line',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-one-expression-per-line'),
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allow: {
|
||||
enum: ['none', 'literal', 'single-child', 'non-jsx'],
|
||||
},
|
||||
},
|
||||
default: optionDefaults,
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = Object.assign({}, optionDefaults, context.options[0]);
|
||||
|
||||
function nodeKey(node) {
|
||||
return `${node.loc.start.line},${node.loc.start.column}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} n
|
||||
* @returns {string}
|
||||
*/
|
||||
function nodeDescriptor(n) {
|
||||
return n.openingElement ? n.openingElement.name.name : getText(context, n).replace(/\n/g, '');
|
||||
}
|
||||
|
||||
function handleJSX(node) {
|
||||
const children = node.children;
|
||||
|
||||
if (!children || !children.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
options.allow === 'non-jsx'
|
||||
&& !children.find((child) => (child.type === 'JSXFragment' || child.type === 'JSXElement'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openingElement = node.openingElement || node.openingFragment;
|
||||
const closingElement = node.closingElement || node.closingFragment;
|
||||
const openingElementStartLine = openingElement.loc.start.line;
|
||||
const openingElementEndLine = openingElement.loc.end.line;
|
||||
const closingElementStartLine = closingElement.loc.start.line;
|
||||
const closingElementEndLine = closingElement.loc.end.line;
|
||||
|
||||
if (children.length === 1) {
|
||||
const child = children[0];
|
||||
if (
|
||||
openingElementStartLine === openingElementEndLine
|
||||
&& openingElementEndLine === closingElementStartLine
|
||||
&& closingElementStartLine === closingElementEndLine
|
||||
&& closingElementEndLine === child.loc.start.line
|
||||
&& child.loc.start.line === child.loc.end.line
|
||||
) {
|
||||
if (
|
||||
options.allow === 'single-child'
|
||||
|| (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const childrenGroupedByLine = {};
|
||||
const fixDetailsByNode = {};
|
||||
|
||||
children.forEach((child) => {
|
||||
let countNewLinesBeforeContent = 0;
|
||||
let countNewLinesAfterContent = 0;
|
||||
|
||||
if (child.type === 'Literal' || child.type === 'JSXText') {
|
||||
if (jsxUtil.isWhiteSpaces(child.raw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
|
||||
countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
|
||||
}
|
||||
|
||||
const startLine = child.loc.start.line + countNewLinesBeforeContent;
|
||||
const endLine = child.loc.end.line - countNewLinesAfterContent;
|
||||
|
||||
if (startLine === endLine) {
|
||||
if (!childrenGroupedByLine[startLine]) {
|
||||
childrenGroupedByLine[startLine] = [];
|
||||
}
|
||||
childrenGroupedByLine[startLine].push(child);
|
||||
} else {
|
||||
if (!childrenGroupedByLine[startLine]) {
|
||||
childrenGroupedByLine[startLine] = [];
|
||||
}
|
||||
childrenGroupedByLine[startLine].push(child);
|
||||
if (!childrenGroupedByLine[endLine]) {
|
||||
childrenGroupedByLine[endLine] = [];
|
||||
}
|
||||
childrenGroupedByLine[endLine].push(child);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(childrenGroupedByLine).forEach((_line) => {
|
||||
const line = parseInt(_line, 10);
|
||||
const firstIndex = 0;
|
||||
const lastIndex = childrenGroupedByLine[line].length - 1;
|
||||
|
||||
childrenGroupedByLine[line].forEach((child, i) => {
|
||||
let prevChild;
|
||||
let nextChild;
|
||||
|
||||
if (i === firstIndex) {
|
||||
if (line === openingElementEndLine) {
|
||||
prevChild = openingElement;
|
||||
}
|
||||
} else {
|
||||
prevChild = childrenGroupedByLine[line][i - 1];
|
||||
}
|
||||
|
||||
if (i === lastIndex) {
|
||||
if (line === closingElementStartLine) {
|
||||
nextChild = closingElement;
|
||||
}
|
||||
} else {
|
||||
// We don't need to append a trailing because the next child will prepend a leading.
|
||||
// nextChild = childrenGroupedByLine[line][i + 1];
|
||||
}
|
||||
|
||||
function spaceBetweenPrev() {
|
||||
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw))
|
||||
|| ((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw))
|
||||
|| getSourceCode(context).isSpaceBetweenTokens(prevChild, child);
|
||||
}
|
||||
|
||||
function spaceBetweenNext() {
|
||||
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw))
|
||||
|| ((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw))
|
||||
|| getSourceCode(context).isSpaceBetweenTokens(child, nextChild);
|
||||
}
|
||||
|
||||
if (!prevChild && !nextChild) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = getText(context, child);
|
||||
const leadingSpace = !!(prevChild && spaceBetweenPrev());
|
||||
const trailingSpace = !!(nextChild && spaceBetweenNext());
|
||||
const leadingNewLine = !!prevChild;
|
||||
const trailingNewLine = !!nextChild;
|
||||
|
||||
const key = nodeKey(child);
|
||||
|
||||
if (!fixDetailsByNode[key]) {
|
||||
fixDetailsByNode[key] = {
|
||||
node: child,
|
||||
source,
|
||||
descriptor: nodeDescriptor(child),
|
||||
};
|
||||
}
|
||||
|
||||
if (leadingSpace) {
|
||||
fixDetailsByNode[key].leadingSpace = true;
|
||||
}
|
||||
if (leadingNewLine) {
|
||||
fixDetailsByNode[key].leadingNewLine = true;
|
||||
}
|
||||
if (trailingNewLine) {
|
||||
fixDetailsByNode[key].trailingNewLine = true;
|
||||
}
|
||||
if (trailingSpace) {
|
||||
fixDetailsByNode[key].trailingSpace = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(fixDetailsByNode).forEach((key) => {
|
||||
const details = fixDetailsByNode[key];
|
||||
|
||||
const nodeToReport = details.node;
|
||||
const descriptor = details.descriptor;
|
||||
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
|
||||
|
||||
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
|
||||
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
|
||||
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
|
||||
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
|
||||
|
||||
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
|
||||
|
||||
report(context, messages.moveToNewLine, 'moveToNewLine', {
|
||||
node: nodeToReport,
|
||||
data: {
|
||||
descriptor,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(nodeToReport, replaceText);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement: handleJSX,
|
||||
JSXFragment: handleJSX,
|
||||
};
|
||||
},
|
||||
};
|
||||
164
node_modules/eslint-plugin-react/lib/rules/jsx-pascal-case.js
generated
vendored
Normal file
164
node_modules/eslint-plugin-react/lib/rules/jsx-pascal-case.js
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @fileoverview Enforce PascalCase for user-defined JSX components
|
||||
* @author Jake Marsh
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
const minimatch = require('minimatch');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
function testDigit(char) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
return charCode >= 48 && charCode <= 57;
|
||||
}
|
||||
|
||||
function testUpperCase(char) {
|
||||
const upperCase = char.toUpperCase();
|
||||
return char === upperCase && upperCase !== char.toLowerCase();
|
||||
}
|
||||
|
||||
function testLowerCase(char) {
|
||||
const lowerCase = char.toLowerCase();
|
||||
return char === lowerCase && lowerCase !== char.toUpperCase();
|
||||
}
|
||||
|
||||
function testPascalCase(name) {
|
||||
if (!testUpperCase(name.charAt(0))) {
|
||||
return false;
|
||||
}
|
||||
const anyNonAlphaNumeric = Array.prototype.some.call(
|
||||
name.slice(1),
|
||||
(char) => char.toLowerCase() === char.toUpperCase() && !testDigit(char)
|
||||
);
|
||||
if (anyNonAlphaNumeric) {
|
||||
return false;
|
||||
}
|
||||
return Array.prototype.some.call(
|
||||
name.slice(1),
|
||||
(char) => testLowerCase(char) || testDigit(char)
|
||||
);
|
||||
}
|
||||
|
||||
function testAllCaps(name) {
|
||||
const firstChar = name.charAt(0);
|
||||
if (!(testUpperCase(firstChar) || testDigit(firstChar))) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 1; i < name.length - 1; i += 1) {
|
||||
const char = name.charAt(i);
|
||||
if (!(testUpperCase(char) || testDigit(char) || char === '_')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const lastChar = name.charAt(name.length - 1);
|
||||
if (!(testUpperCase(lastChar) || testDigit(lastChar))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function ignoreCheck(ignore, name) {
|
||||
return ignore.some(
|
||||
(entry) => name === entry || minimatch(name, entry, { noglobstar: true })
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
usePascalCase: 'Imported JSX component {{name}} must be in PascalCase',
|
||||
usePascalOrSnakeCase: 'Imported JSX component {{name}} must be in PascalCase or SCREAMING_SNAKE_CASE',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce PascalCase for user-defined JSX components',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-pascal-case'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowAllCaps: {
|
||||
type: 'boolean',
|
||||
},
|
||||
allowLeadingUnderscore: {
|
||||
type: 'boolean',
|
||||
},
|
||||
allowNamespace: {
|
||||
type: 'boolean',
|
||||
},
|
||||
ignore: {
|
||||
items: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
minItems: 0,
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowAllCaps = configuration.allowAllCaps || false;
|
||||
const allowLeadingUnderscore = configuration.allowLeadingUnderscore || false;
|
||||
const allowNamespace = configuration.allowNamespace || false;
|
||||
const ignore = configuration.ignore || [];
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
const isCompatTag = jsxUtil.isDOMComponent(node);
|
||||
if (isCompatTag) return undefined;
|
||||
|
||||
const name = elementType(node);
|
||||
let checkNames = [name];
|
||||
let index = 0;
|
||||
|
||||
if (name.lastIndexOf(':') > -1) {
|
||||
checkNames = name.split(':');
|
||||
} else if (name.lastIndexOf('.') > -1) {
|
||||
checkNames = name.split('.');
|
||||
}
|
||||
|
||||
do {
|
||||
const splitName = checkNames[index];
|
||||
if (splitName.length === 1) return undefined;
|
||||
const isIgnored = ignoreCheck(ignore, splitName);
|
||||
|
||||
const checkName = allowLeadingUnderscore && splitName.startsWith('_') ? splitName.slice(1) : splitName;
|
||||
const isPascalCase = testPascalCase(checkName);
|
||||
const isAllowedAllCaps = allowAllCaps && testAllCaps(checkName);
|
||||
|
||||
if (!isPascalCase && !isAllowedAllCaps && !isIgnored) {
|
||||
const messageId = allowAllCaps ? 'usePascalOrSnakeCase' : 'usePascalCase';
|
||||
report(context, messages[messageId], messageId, {
|
||||
node,
|
||||
data: {
|
||||
name: splitName,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
} while (index < checkNames.length && !allowNamespace);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
144
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-multi-spaces.js
generated
vendored
Normal file
144
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-multi-spaces.js
generated
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @fileoverview Disallow multiple spaces between inline JSX props
|
||||
* @author Adrian Moennich
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const report = require('../util/report');
|
||||
const propsUtil = require('../util/props');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”',
|
||||
onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow multiple spaces between inline JSX props',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-props-no-multi-spaces'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
function getPropName(propNode) {
|
||||
switch (propNode.type) {
|
||||
case 'JSXSpreadAttribute':
|
||||
return getText(context, propNode.argument);
|
||||
case 'JSXIdentifier':
|
||||
return propNode.name;
|
||||
case 'JSXMemberExpression':
|
||||
return `${getPropName(propNode.object)}.${propNode.property.name}`;
|
||||
default:
|
||||
return propNode.name
|
||||
? propNode.name.name
|
||||
: `${getText(context, propNode.object)}.${propNode.property.name}`; // needed for typescript-eslint parser
|
||||
}
|
||||
}
|
||||
|
||||
// First and second must be adjacent nodes
|
||||
function hasEmptyLines(first, second) {
|
||||
const comments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(second) : [];
|
||||
const nodes = [].concat(first, comments, second);
|
||||
|
||||
for (let i = 1; i < nodes.length; i += 1) {
|
||||
const prev = nodes[i - 1];
|
||||
const curr = nodes[i];
|
||||
if (curr.loc.start.line - prev.loc.end.line >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkSpacing(prev, node) {
|
||||
if (hasEmptyLines(prev, node)) {
|
||||
report(context, messages.noLineGap, 'noLineGap', {
|
||||
node,
|
||||
data: {
|
||||
prop1: getPropName(prev),
|
||||
prop2: getPropName(node),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (prev.loc.end.line !== node.loc.end.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const between = getSourceCode(context).text.slice(prev.range[1], node.range[0]);
|
||||
|
||||
if (between !== ' ') {
|
||||
report(context, messages.onlyOneSpace, 'onlyOneSpace', {
|
||||
node,
|
||||
data: {
|
||||
prop1: getPropName(prev),
|
||||
prop2: getPropName(node),
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function containsGenericType(node) {
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
if (typeof nodeTypeArguments === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nodeTypeArguments.type === 'TSTypeParameterInstantiation';
|
||||
}
|
||||
|
||||
function getGenericNode(node) {
|
||||
const name = node.name;
|
||||
if (containsGenericType(node)) {
|
||||
const nodeTypeArguments = propsUtil.getTypeArguments(node);
|
||||
|
||||
return Object.assign(
|
||||
{},
|
||||
node,
|
||||
{
|
||||
range: [
|
||||
name.range[0],
|
||||
nodeTypeArguments.range[1],
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
node.attributes.reduce((prev, prop) => {
|
||||
checkSpacing(prev, prop);
|
||||
return prop;
|
||||
}, getGenericNode(node));
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
53
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-spread-multi.js
generated
vendored
Normal file
53
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-spread-multi.js
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @fileoverview Prevent JSX prop spreading the same expression multiple times
|
||||
* @author Simon Schick
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noMultiSpreading: 'Spreading the same expression multiple times is forbidden',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow JSX prop spreading the same identifier multiple times',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-props-no-spread-multi'),
|
||||
},
|
||||
messages,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
const spreads = node.attributes.filter(
|
||||
(attr) => attr.type === 'JSXSpreadAttribute'
|
||||
&& attr.argument.type === 'Identifier'
|
||||
);
|
||||
if (spreads.length < 2) {
|
||||
return;
|
||||
}
|
||||
// We detect duplicate expressions by their identifier
|
||||
const identifierNames = new Set();
|
||||
spreads.forEach((spread) => {
|
||||
if (identifierNames.has(spread.argument.name)) {
|
||||
report(context, messages.noMultiSpreading, 'noMultiSpreading', {
|
||||
node: spread,
|
||||
});
|
||||
}
|
||||
identifierNames.add(spread.argument.name);
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
145
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-spreading.js
generated
vendored
Normal file
145
node_modules/eslint-plugin-react/lib/rules/jsx-props-no-spreading.js
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @fileoverview Prevent JSX prop spreading
|
||||
* @author Ashish Gambhir
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const OPTIONS = { ignore: 'ignore', enforce: 'enforce' };
|
||||
const DEFAULTS = {
|
||||
html: OPTIONS.enforce,
|
||||
custom: OPTIONS.enforce,
|
||||
explicitSpread: OPTIONS.enforce,
|
||||
exceptions: [],
|
||||
};
|
||||
|
||||
const isException = (tag, allExceptions) => allExceptions.indexOf(tag) !== -1;
|
||||
const isProperty = (property) => property.type === 'Property';
|
||||
const getTagNameFromMemberExpression = (node) => {
|
||||
if (node.property.parent) {
|
||||
return `${node.property.parent.object.name}.${node.property.name}`;
|
||||
}
|
||||
// for eslint 3
|
||||
return `${node.object.name}.${node.property.name}`;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noSpreading: 'Prop spreading is forbidden',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow JSX prop spreading',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-props-no-spreading'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
allOf: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
html: {
|
||||
enum: [OPTIONS.enforce, OPTIONS.ignore],
|
||||
},
|
||||
custom: {
|
||||
enum: [OPTIONS.enforce, OPTIONS.ignore],
|
||||
},
|
||||
explicitSpread: {
|
||||
enum: [OPTIONS.enforce, OPTIONS.ignore],
|
||||
},
|
||||
exceptions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
not: {
|
||||
type: 'object',
|
||||
required: ['html', 'custom'],
|
||||
properties: {
|
||||
html: {
|
||||
enum: [OPTIONS.ignore],
|
||||
},
|
||||
custom: {
|
||||
enum: [OPTIONS.ignore],
|
||||
},
|
||||
exceptions: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
maxItems: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreHtmlTags = (configuration.html || DEFAULTS.html) === OPTIONS.ignore;
|
||||
const ignoreCustomTags = (configuration.custom || DEFAULTS.custom) === OPTIONS.ignore;
|
||||
const ignoreExplicitSpread = (configuration.explicitSpread || DEFAULTS.explicitSpread) === OPTIONS.ignore;
|
||||
const exceptions = configuration.exceptions || DEFAULTS.exceptions;
|
||||
return {
|
||||
JSXSpreadAttribute(node) {
|
||||
const jsxOpeningElement = node.parent.name;
|
||||
const type = jsxOpeningElement.type;
|
||||
|
||||
let tagName;
|
||||
if (type === 'JSXIdentifier') {
|
||||
tagName = jsxOpeningElement.name;
|
||||
} else if (type === 'JSXMemberExpression') {
|
||||
tagName = getTagNameFromMemberExpression(jsxOpeningElement);
|
||||
} else {
|
||||
tagName = undefined;
|
||||
}
|
||||
|
||||
const isHTMLTag = tagName && tagName[0] !== tagName[0].toUpperCase();
|
||||
const isCustomTag = tagName && (tagName[0] === tagName[0].toUpperCase() || tagName.includes('.'));
|
||||
if (
|
||||
isHTMLTag
|
||||
&& ((ignoreHtmlTags && !isException(tagName, exceptions))
|
||||
|| (!ignoreHtmlTags && isException(tagName, exceptions)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isCustomTag
|
||||
&& ((ignoreCustomTags && !isException(tagName, exceptions))
|
||||
|| (!ignoreCustomTags && isException(tagName, exceptions)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
ignoreExplicitSpread
|
||||
&& node.argument.type === 'ObjectExpression'
|
||||
&& node.argument.properties.every(isProperty)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.noSpreading, 'noSpreading', {
|
||||
node,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
195
node_modules/eslint-plugin-react/lib/rules/jsx-sort-default-props.js
generated
vendored
Normal file
195
node_modules/eslint-plugin-react/lib/rules/jsx-sort-default-props.js
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @fileoverview Enforce default props alphabetical sorting
|
||||
* @author Vladimir Kattsov
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const log = require('../util/log');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getFirstTokens = eslintUtil.getFirstTokens;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
let isWarnedForDeprecation = false;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propsNotSorted: 'Default prop types declarations should be sorted alphabetically',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: ['sort-default-props'],
|
||||
docs: {
|
||||
description: 'Enforce defaultProps declarations alphabetical sorting',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-sort-default-props'),
|
||||
},
|
||||
// fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreCase: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
|
||||
/**
|
||||
* Get properties name
|
||||
* @param {Object} node - Property.
|
||||
* @returns {string} Property name.
|
||||
*/
|
||||
function getPropertyName(node) {
|
||||
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
|
||||
return node.key.name;
|
||||
}
|
||||
if (node.type === 'MemberExpression') {
|
||||
return node.property.name;
|
||||
// Special case for class properties
|
||||
// (babel-eslint@5 does not expose property name so we have to rely on tokens)
|
||||
}
|
||||
if (node.type === 'ClassProperty') {
|
||||
const tokens = getFirstTokens(context, node, 2);
|
||||
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Identifier node passed in looks like a defaultProps declaration.
|
||||
* @param {ASTNode} node The node to check. Must be an Identifier node.
|
||||
* @returns {boolean} `true` if the node is a defaultProps declaration, `false` if not
|
||||
*/
|
||||
function isDefaultPropsDeclaration(node) {
|
||||
const propName = getPropertyName(node);
|
||||
return (propName === 'defaultProps' || propName === 'getDefaultProps');
|
||||
}
|
||||
|
||||
function getKey(node) {
|
||||
return getText(context, node.key || node.argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable by name in the current scope.
|
||||
* @param {ASTNode} node The node to look for.
|
||||
* @param {string} name Name of the variable to look for.
|
||||
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
|
||||
*/
|
||||
function findVariableByName(node, name) {
|
||||
const variable = variableUtil
|
||||
.getVariableFromContext(context, node, name);
|
||||
|
||||
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (variable.defs[0].node.type === 'TypeAlias') {
|
||||
return variable.defs[0].node.right;
|
||||
}
|
||||
|
||||
return variable.defs[0].node.init;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if defaultProps declarations are sorted
|
||||
* @param {Array} declarations The array of AST nodes being checked.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSorted(declarations) {
|
||||
// function fix(fixer) {
|
||||
// return propTypesSortUtil.fixPropTypesSort(context, fixer, declarations, ignoreCase);
|
||||
// }
|
||||
|
||||
declarations.reduce((prev, curr, idx, decls) => {
|
||||
if (/Spread(?:Property|Element)$/.test(curr.type)) {
|
||||
return decls[idx + 1];
|
||||
}
|
||||
|
||||
let prevPropName = getKey(prev);
|
||||
let currentPropName = getKey(curr);
|
||||
|
||||
if (ignoreCase) {
|
||||
prevPropName = prevPropName.toLowerCase();
|
||||
currentPropName = currentPropName.toLowerCase();
|
||||
}
|
||||
|
||||
if (currentPropName < prevPropName) {
|
||||
report(context, messages.propsNotSorted, 'propsNotSorted', {
|
||||
node: curr,
|
||||
// fix
|
||||
});
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
}, declarations[0]);
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.type === 'ObjectExpression') {
|
||||
checkSorted(node.properties);
|
||||
} else if (node.type === 'Identifier') {
|
||||
const propTypesObject = findVariableByName(node, node.name);
|
||||
if (propTypesObject && propTypesObject.properties) {
|
||||
checkSorted(propTypesObject.properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!isDefaultPropsDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode(node.value);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!isDefaultPropsDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode('right' in node.parent && node.parent.right);
|
||||
},
|
||||
|
||||
Program() {
|
||||
if (isWarnedForDeprecation) {
|
||||
return;
|
||||
}
|
||||
|
||||
log('The react/jsx-sort-default-props rule is deprecated. It has been renamed to `react/sort-default-props`.');
|
||||
isWarnedForDeprecation = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
531
node_modules/eslint-plugin-react/lib/rules/jsx-sort-props.js
generated
vendored
Normal file
531
node_modules/eslint-plugin-react/lib/rules/jsx-sort-props.js
generated
vendored
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* @fileoverview Enforce props alphabetical sorting
|
||||
* @author Ilya Volodin, Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const propName = require('jsx-ast-utils/propName');
|
||||
const includes = require('array-includes');
|
||||
const toSorted = require('array.prototype.tosorted');
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
const propTypesSortUtil = require('../util/propTypesSort');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getText = eslintUtil.getText;
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function isMultilineProp(node) {
|
||||
return node.loc.start.line !== node.loc.end.line;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
|
||||
listIsEmpty: 'A customized reserved first list must not be empty',
|
||||
listReservedPropsFirst: 'Reserved props must be listed before all other props',
|
||||
listCallbacksLast: 'Callbacks must be listed after all other props',
|
||||
listShorthandFirst: 'Shorthand props must be listed before all other props',
|
||||
listShorthandLast: 'Shorthand props must be listed after all other props',
|
||||
listMultilineFirst: 'Multiline props must be listed before all other props',
|
||||
listMultilineLast: 'Multiline props must be listed after all other props',
|
||||
sortPropsByAlpha: 'Props should be sorted alphabetically',
|
||||
};
|
||||
|
||||
const RESERVED_PROPS_LIST = [
|
||||
'children',
|
||||
'dangerouslySetInnerHTML',
|
||||
'key',
|
||||
'ref',
|
||||
];
|
||||
|
||||
function isReservedPropName(name, list) {
|
||||
return list.indexOf(name) >= 0;
|
||||
}
|
||||
|
||||
let attributeMap;
|
||||
// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end }
|
||||
|
||||
function shouldSortToEnd(node) {
|
||||
const attr = attributeMap.get(node);
|
||||
return !!attr && !!attr.hasComment;
|
||||
}
|
||||
|
||||
function contextCompare(a, b, options) {
|
||||
let aProp = propName(a);
|
||||
let bProp = propName(b);
|
||||
|
||||
const aSortToEnd = shouldSortToEnd(a);
|
||||
const bSortToEnd = shouldSortToEnd(b);
|
||||
if (aSortToEnd && !bSortToEnd) {
|
||||
return 1;
|
||||
}
|
||||
if (!aSortToEnd && bSortToEnd) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (options.reservedFirst) {
|
||||
const aIsReserved = isReservedPropName(aProp, options.reservedList);
|
||||
const bIsReserved = isReservedPropName(bProp, options.reservedList);
|
||||
if (aIsReserved && !bIsReserved) {
|
||||
return -1;
|
||||
}
|
||||
if (!aIsReserved && bIsReserved) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.callbacksLast) {
|
||||
const aIsCallback = propTypesSortUtil.isCallbackPropName(aProp);
|
||||
const bIsCallback = propTypesSortUtil.isCallbackPropName(bProp);
|
||||
if (aIsCallback && !bIsCallback) {
|
||||
return 1;
|
||||
}
|
||||
if (!aIsCallback && bIsCallback) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.shorthandFirst || options.shorthandLast) {
|
||||
const shorthandSign = options.shorthandFirst ? -1 : 1;
|
||||
if (!a.value && b.value) {
|
||||
return shorthandSign;
|
||||
}
|
||||
if (a.value && !b.value) {
|
||||
return -shorthandSign;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.multiline !== 'ignore') {
|
||||
const multilineSign = options.multiline === 'first' ? -1 : 1;
|
||||
const aIsMultiline = isMultilineProp(a);
|
||||
const bIsMultiline = isMultilineProp(b);
|
||||
if (aIsMultiline && !bIsMultiline) {
|
||||
return multilineSign;
|
||||
}
|
||||
if (!aIsMultiline && bIsMultiline) {
|
||||
return -multilineSign;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.noSortAlphabetically) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const actualLocale = options.locale === 'auto' ? undefined : options.locale;
|
||||
|
||||
if (options.ignoreCase) {
|
||||
aProp = aProp.toLowerCase();
|
||||
bProp = bProp.toLowerCase();
|
||||
return aProp.localeCompare(bProp, actualLocale);
|
||||
}
|
||||
if (aProp === bProp) {
|
||||
return 0;
|
||||
}
|
||||
if (options.locale === 'auto') {
|
||||
return aProp < bProp ? -1 : 1;
|
||||
}
|
||||
return aProp.localeCompare(bProp, actualLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of arrays where each subarray is composed of attributes
|
||||
* that are considered sortable.
|
||||
* @param {Array<JSXSpreadAttribute|JSXAttribute>} attributes
|
||||
* @param {Object} context The context of the rule
|
||||
* @return {Array<Array<JSXAttribute>>}
|
||||
*/
|
||||
function getGroupsOfSortableAttributes(attributes, context) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
const sortableAttributeGroups = [];
|
||||
let groupCount = 0;
|
||||
function addtoSortableAttributeGroups(attribute) {
|
||||
sortableAttributeGroups[groupCount - 1].push(attribute);
|
||||
}
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attribute = attributes[i];
|
||||
const nextAttribute = attributes[i + 1];
|
||||
const attributeline = attribute.loc.start.line;
|
||||
let comment = [];
|
||||
try {
|
||||
comment = sourceCode.getCommentsAfter(attribute);
|
||||
} catch (e) { /**/ }
|
||||
const lastAttr = attributes[i - 1];
|
||||
const attrIsSpread = attribute.type === 'JSXSpreadAttribute';
|
||||
|
||||
// If we have no groups or if the last attribute was JSXSpreadAttribute
|
||||
// then we start a new group. Append attributes to the group until we
|
||||
// come across another JSXSpreadAttribute or exhaust the array.
|
||||
if (
|
||||
!lastAttr
|
||||
|| (lastAttr.type === 'JSXSpreadAttribute' && !attrIsSpread)
|
||||
) {
|
||||
groupCount += 1;
|
||||
sortableAttributeGroups[groupCount - 1] = [];
|
||||
}
|
||||
if (!attrIsSpread) {
|
||||
if (comment.length === 0) {
|
||||
attributeMap.set(attribute, { end: attribute.range[1], hasComment: false });
|
||||
addtoSortableAttributeGroups(attribute);
|
||||
} else {
|
||||
const firstComment = comment[0];
|
||||
const commentline = firstComment.loc.start.line;
|
||||
if (comment.length === 1) {
|
||||
if (attributeline + 1 === commentline && nextAttribute) {
|
||||
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
|
||||
addtoSortableAttributeGroups(attribute);
|
||||
i += 1;
|
||||
} else if (attributeline === commentline) {
|
||||
if (firstComment.type === 'Block' && nextAttribute) {
|
||||
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
|
||||
i += 1;
|
||||
} else if (firstComment.type === 'Block') {
|
||||
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: true });
|
||||
} else {
|
||||
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: false });
|
||||
}
|
||||
addtoSortableAttributeGroups(attribute);
|
||||
}
|
||||
} else if (comment.length > 1 && attributeline + 1 === comment[1].loc.start.line && nextAttribute) {
|
||||
const commentNextAttribute = sourceCode.getCommentsAfter(nextAttribute);
|
||||
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true });
|
||||
if (
|
||||
commentNextAttribute.length === 1
|
||||
&& nextAttribute.loc.start.line === commentNextAttribute[0].loc.start.line
|
||||
) {
|
||||
attributeMap.set(attribute, { end: commentNextAttribute[0].range[1], hasComment: true });
|
||||
}
|
||||
addtoSortableAttributeGroups(attribute);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortableAttributeGroups;
|
||||
}
|
||||
|
||||
function generateFixerFunction(node, context, reservedList) {
|
||||
const attributes = node.attributes.slice(0);
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
const callbacksLast = configuration.callbacksLast || false;
|
||||
const shorthandFirst = configuration.shorthandFirst || false;
|
||||
const shorthandLast = configuration.shorthandLast || false;
|
||||
const multiline = configuration.multiline || 'ignore';
|
||||
const noSortAlphabetically = configuration.noSortAlphabetically || false;
|
||||
const reservedFirst = configuration.reservedFirst || false;
|
||||
const locale = configuration.locale || 'auto';
|
||||
|
||||
// Sort props according to the context. Only supports ignoreCase.
|
||||
// Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides),
|
||||
// we only consider groups of sortable attributes.
|
||||
const options = {
|
||||
ignoreCase,
|
||||
callbacksLast,
|
||||
shorthandFirst,
|
||||
shorthandLast,
|
||||
multiline,
|
||||
noSortAlphabetically,
|
||||
reservedFirst,
|
||||
reservedList,
|
||||
locale,
|
||||
};
|
||||
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context);
|
||||
const sortedAttributeGroups = sortableAttributeGroups
|
||||
.slice(0)
|
||||
.map((group) => toSorted(group, (a, b) => contextCompare(a, b, options)));
|
||||
|
||||
return function fixFunction(fixer) {
|
||||
const fixers = [];
|
||||
let source = getText(context);
|
||||
|
||||
sortableAttributeGroups.forEach((sortableGroup, ii) => {
|
||||
sortableGroup.forEach((attr, jj) => {
|
||||
const sortedAttr = sortedAttributeGroups[ii][jj];
|
||||
const sortedAttrText = source.slice(sortedAttr.range[0], attributeMap.get(sortedAttr).end);
|
||||
fixers.push({
|
||||
range: [attr.range[0], attributeMap.get(attr).end],
|
||||
text: sortedAttrText,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fixers.sort((a, b) => b.range[0] - a.range[0]);
|
||||
|
||||
const firstFixer = fixers[0];
|
||||
const lastFixer = fixers[fixers.length - 1];
|
||||
const rangeStart = lastFixer ? lastFixer.range[0] : 0;
|
||||
const rangeEnd = firstFixer ? firstFixer.range[1] : -0;
|
||||
|
||||
fixers.forEach((fix) => {
|
||||
source = `${source.slice(0, fix.range[0])}${fix.text}${source.slice(fix.range[1])}`;
|
||||
});
|
||||
|
||||
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the `reservedFirst` option is valid
|
||||
* @param {Object} context The context of the rule
|
||||
* @param {boolean | string[]} reservedFirst The `reservedFirst` option
|
||||
* @return {Function | undefined} If an error is detected, a function to generate the error message, otherwise, `undefined`
|
||||
*/
|
||||
// eslint-disable-next-line consistent-return
|
||||
function validateReservedFirstConfig(context, reservedFirst) {
|
||||
if (reservedFirst) {
|
||||
if (Array.isArray(reservedFirst)) {
|
||||
// Only allow a subset of reserved words in customized lists
|
||||
const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName(
|
||||
word,
|
||||
RESERVED_PROPS_LIST
|
||||
));
|
||||
|
||||
if (reservedFirst.length === 0) {
|
||||
return function Report(decl) {
|
||||
report(context, messages.listIsEmpty, 'listIsEmpty', {
|
||||
node: decl,
|
||||
});
|
||||
};
|
||||
}
|
||||
if (nonReservedWords.length > 0) {
|
||||
return function Report(decl) {
|
||||
report(context, messages.noUnreservedProps, 'noUnreservedProps', {
|
||||
node: decl,
|
||||
data: {
|
||||
unreservedWords: nonReservedWords.toString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportedNodeAttributes = new WeakMap();
|
||||
/**
|
||||
* Check if the current node attribute has already been reported with the same error type
|
||||
* if that's the case then we don't report a new error
|
||||
* otherwise we report the error
|
||||
* @param {Object} nodeAttribute The node attribute to be reported
|
||||
* @param {string} errorType The error type to be reported
|
||||
* @param {Object} node The parent node for the node attribute
|
||||
* @param {Object} context The context of the rule
|
||||
* @param {Array<String>} reservedList The list of reserved props
|
||||
*/
|
||||
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
|
||||
const errors = reportedNodeAttributes.get(nodeAttribute) || [];
|
||||
|
||||
if (includes(errors, errorType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
errors.push(errorType);
|
||||
|
||||
reportedNodeAttributes.set(nodeAttribute, errors);
|
||||
|
||||
report(context, messages[errorType], errorType, {
|
||||
node: nodeAttribute.name,
|
||||
fix: generateFixerFunction(node, context, reservedList),
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce props alphabetical sorting',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-sort-props'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
// Whether callbacks (prefixed with "on") should be listed at the very end,
|
||||
// after all other props. Supersedes shorthandLast.
|
||||
callbacksLast: {
|
||||
type: 'boolean',
|
||||
},
|
||||
// Whether shorthand properties (without a value) should be listed first
|
||||
shorthandFirst: {
|
||||
type: 'boolean',
|
||||
},
|
||||
// Whether shorthand properties (without a value) should be listed last
|
||||
shorthandLast: {
|
||||
type: 'boolean',
|
||||
},
|
||||
// Whether multiline properties should be listed first or last
|
||||
multiline: {
|
||||
enum: ['ignore', 'first', 'last'],
|
||||
default: 'ignore',
|
||||
},
|
||||
ignoreCase: {
|
||||
type: 'boolean',
|
||||
},
|
||||
// Whether alphabetical sorting should be enforced
|
||||
noSortAlphabetically: {
|
||||
type: 'boolean',
|
||||
},
|
||||
reservedFirst: {
|
||||
type: ['array', 'boolean'],
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
default: 'auto',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
const callbacksLast = configuration.callbacksLast || false;
|
||||
const shorthandFirst = configuration.shorthandFirst || false;
|
||||
const shorthandLast = configuration.shorthandLast || false;
|
||||
const multiline = configuration.multiline || 'ignore';
|
||||
const noSortAlphabetically = configuration.noSortAlphabetically || false;
|
||||
const reservedFirst = configuration.reservedFirst || false;
|
||||
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
|
||||
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
|
||||
const locale = configuration.locale || 'auto';
|
||||
|
||||
return {
|
||||
Program() {
|
||||
attributeMap = new WeakMap();
|
||||
},
|
||||
|
||||
JSXOpeningElement(node) {
|
||||
// `dangerouslySetInnerHTML` is only "reserved" on DOM components
|
||||
const nodeReservedList = reservedFirst && !jsxUtil.isDOMComponent(node) ? reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML') : reservedList;
|
||||
|
||||
node.attributes.reduce((memo, decl, idx, attrs) => {
|
||||
if (decl.type === 'JSXSpreadAttribute') {
|
||||
return attrs[idx + 1];
|
||||
}
|
||||
|
||||
let previousPropName = propName(memo);
|
||||
let currentPropName = propName(decl);
|
||||
const previousValue = memo.value;
|
||||
const currentValue = decl.value;
|
||||
const previousIsCallback = propTypesSortUtil.isCallbackPropName(previousPropName);
|
||||
const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName);
|
||||
|
||||
if (ignoreCase) {
|
||||
previousPropName = previousPropName.toLowerCase();
|
||||
currentPropName = currentPropName.toLowerCase();
|
||||
}
|
||||
|
||||
if (reservedFirst) {
|
||||
if (reservedFirstError) {
|
||||
reservedFirstError(decl);
|
||||
return memo;
|
||||
}
|
||||
|
||||
const previousIsReserved = isReservedPropName(previousPropName, nodeReservedList);
|
||||
const currentIsReserved = isReservedPropName(currentPropName, nodeReservedList);
|
||||
|
||||
if (previousIsReserved && !currentIsReserved) {
|
||||
return decl;
|
||||
}
|
||||
if (!previousIsReserved && currentIsReserved) {
|
||||
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (callbacksLast) {
|
||||
if (!previousIsCallback && currentIsCallback) {
|
||||
// Entering the callback prop section
|
||||
return decl;
|
||||
}
|
||||
if (previousIsCallback && !currentIsCallback) {
|
||||
// Encountered a non-callback prop after a callback prop
|
||||
reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (shorthandFirst) {
|
||||
if (currentValue && !previousValue) {
|
||||
return decl;
|
||||
}
|
||||
if (!currentValue && previousValue) {
|
||||
reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (shorthandLast) {
|
||||
if (!currentValue && previousValue) {
|
||||
return decl;
|
||||
}
|
||||
if (currentValue && !previousValue) {
|
||||
reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
const previousIsMultiline = isMultilineProp(memo);
|
||||
const currentIsMultiline = isMultilineProp(decl);
|
||||
if (multiline === 'first') {
|
||||
if (previousIsMultiline && !currentIsMultiline) {
|
||||
// Exiting the multiline prop section
|
||||
return decl;
|
||||
}
|
||||
if (!previousIsMultiline && currentIsMultiline) {
|
||||
// Encountered a non-multiline prop before a multiline prop
|
||||
reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
} else if (multiline === 'last') {
|
||||
if (!previousIsMultiline && currentIsMultiline) {
|
||||
// Entering the multiline prop section
|
||||
return decl;
|
||||
}
|
||||
if (previousIsMultiline && !currentIsMultiline) {
|
||||
// Encountered a non-multiline prop after a multiline prop
|
||||
reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!noSortAlphabetically
|
||||
&& (
|
||||
(ignoreCase || locale !== 'auto')
|
||||
? previousPropName.localeCompare(currentPropName, locale === 'auto' ? undefined : locale) > 0
|
||||
: previousPropName > currentPropName
|
||||
)
|
||||
) {
|
||||
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList);
|
||||
|
||||
return memo;
|
||||
}
|
||||
|
||||
return decl;
|
||||
}, node.attributes[0]);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
98
node_modules/eslint-plugin-react/lib/rules/jsx-space-before-closing.js
generated
vendored
Normal file
98
node_modules/eslint-plugin-react/lib/rules/jsx-space-before-closing.js
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @fileoverview Validate spacing before closing bracket in JSX.
|
||||
* @author ryym
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const log = require('../util/log');
|
||||
const report = require('../util/report');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
|
||||
let isWarnedForDeprecation = false;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noSpaceBeforeClose: 'A space is forbidden before closing bracket',
|
||||
needSpaceBeforeClose: 'A space is required before closing bracket',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
deprecated: true,
|
||||
replacedBy: ['jsx-tag-spacing'],
|
||||
docs: {
|
||||
description: 'Enforce spacing before closing bracket in JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-space-before-closing'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
enum: ['always', 'never'],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || 'always';
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (!node.selfClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
const leftToken = getTokenBeforeClosingBracket(node);
|
||||
const closingSlash = /** @type {import("eslint").AST.Token} */ (sourceCode.getTokenAfter(leftToken));
|
||||
|
||||
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
|
||||
report(context, messages.needSpaceBeforeClose, 'needSpaceBeforeClose', {
|
||||
loc: closingSlash.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingSlash, ' ');
|
||||
},
|
||||
});
|
||||
} else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
|
||||
report(context, messages.noSpaceBeforeClose, 'noSpaceBeforeClose', {
|
||||
loc: closingSlash.loc.start,
|
||||
fix(fixer) {
|
||||
const previousToken = sourceCode.getTokenBefore(closingSlash);
|
||||
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Program() {
|
||||
if (isWarnedForDeprecation) {
|
||||
return;
|
||||
}
|
||||
|
||||
log('The react/jsx-space-before-closing rule is deprecated. '
|
||||
+ 'Please use the react/jsx-tag-spacing rule with the '
|
||||
+ '"beforeSelfClosing" option instead.');
|
||||
isWarnedForDeprecation = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
328
node_modules/eslint-plugin-react/lib/rules/jsx-tag-spacing.js
generated
vendored
Normal file
328
node_modules/eslint-plugin-react/lib/rules/jsx-tag-spacing.js
generated
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
|
||||
* @author Diogo Franco (Kovensky)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getFirstTokens = eslintUtil.getFirstTokens;
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
|
||||
const messages = {
|
||||
selfCloseSlashNoSpace: 'Whitespace is forbidden between `/` and `>`; write `/>`',
|
||||
selfCloseSlashNeedSpace: 'Whitespace is required between `/` and `>`; write `/ >`',
|
||||
closeSlashNoSpace: 'Whitespace is forbidden between `<` and `/`; write `</`',
|
||||
closeSlashNeedSpace: 'Whitespace is required between `<` and `/`; write `< /`',
|
||||
beforeSelfCloseNoSpace: 'A space is forbidden before closing bracket',
|
||||
beforeSelfCloseNeedSpace: 'A space is required before closing bracket',
|
||||
beforeSelfCloseNeedNewline: 'A newline is required before closing bracket',
|
||||
afterOpenNoSpace: 'A space is forbidden after opening bracket',
|
||||
afterOpenNeedSpace: 'A space is required after opening bracket',
|
||||
beforeCloseNoSpace: 'A space is forbidden before closing bracket',
|
||||
beforeCloseNeedSpace: 'Whitespace is required before closing bracket',
|
||||
beforeCloseNeedNewline: 'A newline is required before closing bracket',
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Validators
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function validateClosingSlash(context, node, option) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
let adjacent;
|
||||
|
||||
if (node.selfClosing) {
|
||||
const lastTokens = sourceCode.getLastTokens(node, 2);
|
||||
|
||||
adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
|
||||
|
||||
if (option === 'never') {
|
||||
if (!adjacent) {
|
||||
report(context, messages.selfCloseSlashNoSpace, 'selfCloseSlashNoSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: lastTokens[0].loc.start,
|
||||
end: lastTokens[1].loc.end,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (option === 'always' && adjacent) {
|
||||
report(context, messages.selfCloseSlashNeedSpace, 'selfCloseSlashNeedSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: lastTokens[0].loc.start,
|
||||
end: lastTokens[1].loc.end,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(lastTokens[1], ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const firstTokens = getFirstTokens(context, node, 2);
|
||||
|
||||
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
|
||||
|
||||
if (option === 'never') {
|
||||
if (!adjacent) {
|
||||
report(context, messages.closeSlashNoSpace, 'closeSlashNoSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: firstTokens[0].loc.start,
|
||||
end: firstTokens[1].loc.end,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (option === 'always' && adjacent) {
|
||||
report(context, messages.closeSlashNeedSpace, 'closeSlashNeedSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: firstTokens[0].loc.start,
|
||||
end: firstTokens[1].loc.end,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(firstTokens[1], ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateBeforeSelfClosing(context, node, option) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const leftToken = getTokenBeforeClosingBracket(node);
|
||||
const closingSlash = sourceCode.getTokenAfter(leftToken);
|
||||
|
||||
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
|
||||
if (leftToken.loc.end.line === closingSlash.loc.start.line) {
|
||||
report(context, messages.beforeSelfCloseNeedNewline, 'beforeSelfCloseNeedNewline', {
|
||||
node,
|
||||
loc: leftToken.loc.end,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingSlash, '\n');
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash);
|
||||
|
||||
if ((option === 'always' || option === 'proportional-always') && adjacent) {
|
||||
report(context, messages.beforeSelfCloseNeedSpace, 'beforeSelfCloseNeedSpace', {
|
||||
node,
|
||||
loc: closingSlash.loc.start,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingSlash, ' ');
|
||||
},
|
||||
});
|
||||
} else if (option === 'never' && !adjacent) {
|
||||
report(context, messages.beforeSelfCloseNoSpace, 'beforeSelfCloseNoSpace', {
|
||||
node,
|
||||
loc: closingSlash.loc.start,
|
||||
fix(fixer) {
|
||||
const previousToken = sourceCode.getTokenBefore(closingSlash);
|
||||
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateAfterOpening(context, node, option) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const openingToken = sourceCode.getTokenBefore(node.name);
|
||||
|
||||
if (option === 'allow-multiline') {
|
||||
if (openingToken.loc.start.line !== node.name.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
|
||||
|
||||
if (option === 'never' || option === 'allow-multiline') {
|
||||
if (!adjacent) {
|
||||
report(context, messages.afterOpenNoSpace, 'afterOpenNoSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: openingToken.loc.start,
|
||||
end: node.name.loc.start,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (option === 'always' && adjacent) {
|
||||
report(context, messages.afterOpenNeedSpace, 'afterOpenNeedSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: openingToken.loc.start,
|
||||
end: node.name.loc.start,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(node.name, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateBeforeClosing(context, node, option) {
|
||||
// Don't enforce this rule for self closing tags
|
||||
if (!node.selfClosing) {
|
||||
const sourceCode = getSourceCode(context);
|
||||
const leftToken = option === 'proportional-always'
|
||||
? getTokenBeforeClosingBracket(node)
|
||||
: sourceCode.getLastTokens(node, 2)[0];
|
||||
const closingToken = sourceCode.getTokenAfter(leftToken);
|
||||
|
||||
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
|
||||
if (leftToken.loc.end.line === closingToken.loc.start.line) {
|
||||
report(context, messages.beforeCloseNeedNewline, 'beforeCloseNeedNewline', {
|
||||
node,
|
||||
loc: leftToken.loc.end,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingToken, '\n');
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (leftToken.loc.start.line !== closingToken.loc.start.line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
|
||||
|
||||
if (option === 'never' && !adjacent) {
|
||||
report(context, messages.beforeCloseNoSpace, 'beforeCloseNoSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: leftToken.loc.end,
|
||||
end: closingToken.loc.start,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
|
||||
},
|
||||
});
|
||||
} else if (option === 'always' && adjacent) {
|
||||
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: leftToken.loc.end,
|
||||
end: closingToken.loc.start,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingToken, ' ');
|
||||
},
|
||||
});
|
||||
} else if (option === 'proportional-always' && node.type === 'JSXOpeningElement' && adjacent !== (node.loc.start.line === node.loc.end.line)) {
|
||||
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
|
||||
node,
|
||||
loc: {
|
||||
start: leftToken.loc.end,
|
||||
end: closingToken.loc.start,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closingToken, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const optionDefaults = {
|
||||
closingSlash: 'never',
|
||||
beforeSelfClosing: 'always',
|
||||
afterOpening: 'never',
|
||||
beforeClosing: 'allow',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce whitespace in and around the JSX opening and closing brackets',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-tag-spacing'),
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
closingSlash: {
|
||||
enum: ['always', 'never', 'allow'],
|
||||
},
|
||||
beforeSelfClosing: {
|
||||
enum: ['always', 'proportional-always', 'never', 'allow'],
|
||||
},
|
||||
afterOpening: {
|
||||
enum: ['always', 'allow-multiline', 'never', 'allow'],
|
||||
},
|
||||
beforeClosing: {
|
||||
enum: ['always', 'proportional-always', 'never', 'allow'],
|
||||
},
|
||||
},
|
||||
default: optionDefaults,
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const options = Object.assign({}, optionDefaults, context.options[0]);
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (options.closingSlash !== 'allow' && node.selfClosing) {
|
||||
validateClosingSlash(context, node, options.closingSlash);
|
||||
}
|
||||
if (options.afterOpening !== 'allow') {
|
||||
validateAfterOpening(context, node, options.afterOpening);
|
||||
}
|
||||
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
|
||||
validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
|
||||
}
|
||||
if (options.beforeClosing !== 'allow') {
|
||||
validateBeforeClosing(context, node, options.beforeClosing);
|
||||
}
|
||||
},
|
||||
JSXClosingElement(node) {
|
||||
if (options.afterOpening !== 'allow') {
|
||||
validateAfterOpening(context, node, options.afterOpening);
|
||||
}
|
||||
if (options.closingSlash !== 'allow') {
|
||||
validateClosingSlash(context, node, options.closingSlash);
|
||||
}
|
||||
if (options.beforeClosing !== 'allow') {
|
||||
validateBeforeClosing(context, node, options.beforeClosing);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
52
node_modules/eslint-plugin-react/lib/rules/jsx-uses-react.js
generated
vendored
Normal file
52
node_modules/eslint-plugin-react/lib/rules/jsx-uses-react.js
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @fileoverview Prevent React to be marked as unused
|
||||
* @author Glen Mailer
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const markVariableAsUsed = require('../util/eslint').markVariableAsUsed;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
// eslint-disable-next-line eslint-plugin/prefer-message-ids -- https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/292
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow React to be incorrectly marked as unused',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-uses-react'),
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
const fragment = pragmaUtil.getFragmentFromContext(context);
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleOpeningElement(node) {
|
||||
markVariableAsUsed(pragma, node, context);
|
||||
}
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
JSXOpeningElement: handleOpeningElement,
|
||||
JSXOpeningFragment: handleOpeningElement,
|
||||
JSXFragment(node) {
|
||||
markVariableAsUsed(fragment, node, context);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
62
node_modules/eslint-plugin-react/lib/rules/jsx-uses-vars.js
generated
vendored
Normal file
62
node_modules/eslint-plugin-react/lib/rules/jsx-uses-vars.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @fileoverview Prevent variables used in JSX to be marked as unused
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const markVariableAsUsed = require('../util/eslint').markVariableAsUsed;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const isTagNameRe = /^[a-z]/;
|
||||
const isTagName = (name) => isTagNameRe.test(name);
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
// eslint-disable-next-line eslint-plugin/prefer-message-ids -- https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/292
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow variables used in JSX to be incorrectly marked as unused',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('jsx-uses-vars'),
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
let name;
|
||||
if (node.name.namespace) {
|
||||
// <Foo:Bar>
|
||||
return;
|
||||
}
|
||||
if (node.name.name) {
|
||||
// <Foo>
|
||||
name = node.name.name;
|
||||
// Exclude lowercase tag names like <div>
|
||||
if (isTagName(name)) {
|
||||
return;
|
||||
}
|
||||
} else if (node.name.object) {
|
||||
// <Foo...Bar>
|
||||
let parent = node.name.object;
|
||||
while (parent.object) {
|
||||
parent = parent.object;
|
||||
}
|
||||
name = parent.name;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
markVariableAsUsed(name, node, context);
|
||||
},
|
||||
|
||||
};
|
||||
},
|
||||
};
|
||||
275
node_modules/eslint-plugin-react/lib/rules/jsx-wrap-multilines.js
generated
vendored
Normal file
275
node_modules/eslint-plugin-react/lib/rules/jsx-wrap-multilines.js
generated
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* @fileoverview Prevent missing parentheses around multilines JSX
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const reportC = require('../util/report');
|
||||
const isParenthesized = require('../util/ast').isParenthesized;
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = {
|
||||
declaration: 'parens',
|
||||
assignment: 'parens',
|
||||
return: 'parens',
|
||||
arrow: 'parens',
|
||||
condition: 'ignore',
|
||||
logical: 'ignore',
|
||||
prop: 'ignore',
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
missingParens: 'Missing parentheses around multilines JSX',
|
||||
extraParens: 'Expected no parentheses around multilines JSX',
|
||||
parensOnNewLines: 'Parentheses around JSX should be on separate lines',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing parentheses around multiline JSX',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('jsx-wrap-multilines'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
// true/false are for backwards compatibility
|
||||
properties: {
|
||||
declaration: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
assignment: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
return: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
arrow: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
condition: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
logical: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
prop: {
|
||||
enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function getOption(type) {
|
||||
const userOptions = context.options[0] || {};
|
||||
if (has(userOptions, type)) {
|
||||
return userOptions[type];
|
||||
}
|
||||
return DEFAULTS[type];
|
||||
}
|
||||
|
||||
function isEnabled(type) {
|
||||
const option = getOption(type);
|
||||
return option && option !== 'ignore';
|
||||
}
|
||||
|
||||
function needsOpeningNewLine(node) {
|
||||
const previousToken = getSourceCode(context).getTokenBefore(node);
|
||||
|
||||
if (!isParenthesized(context, node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousToken.loc.end.line === node.loc.start.line) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function needsClosingNewLine(node) {
|
||||
const nextToken = getSourceCode(context).getTokenAfter(node);
|
||||
|
||||
if (!isParenthesized(context, node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.loc.end.line === nextToken.loc.end.line) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMultilines(node) {
|
||||
return node.loc.start.line !== node.loc.end.line;
|
||||
}
|
||||
|
||||
function report(node, messageId, fix) {
|
||||
reportC(context, messages[messageId], messageId, {
|
||||
node,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
|
||||
function trimTokenBeforeNewline(node, tokenBefore) {
|
||||
// if the token before the jsx is a bracket or curly brace
|
||||
// we don't want a space between the opening parentheses and the multiline jsx
|
||||
const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
|
||||
return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
|
||||
}
|
||||
|
||||
function check(node, type) {
|
||||
if (!node || !jsxUtil.isJSX(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCode = getSourceCode(context);
|
||||
const option = getOption(type);
|
||||
|
||||
if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) {
|
||||
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${getText(context, node)})`));
|
||||
}
|
||||
|
||||
if (option === 'parens-new-line' && isMultilines(node)) {
|
||||
if (!isParenthesized(context, node)) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
|
||||
const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
|
||||
const start = node.loc.start;
|
||||
if (tokenBefore.loc.end.line < start.line) {
|
||||
// Strip newline after operator if parens newline is specified
|
||||
report(
|
||||
node,
|
||||
'missingParens',
|
||||
(fixer) => fixer.replaceTextRange(
|
||||
[tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
|
||||
`${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${getText(context, node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${getText(context, node)}\n)`));
|
||||
}
|
||||
} else {
|
||||
const needsOpening = needsOpeningNewLine(node);
|
||||
const needsClosing = needsClosingNewLine(node);
|
||||
if (needsOpening || needsClosing) {
|
||||
report(node, 'parensOnNewLines', (fixer) => {
|
||||
const text = getText(context, node);
|
||||
let fixed = text;
|
||||
if (needsOpening) {
|
||||
fixed = `\n${fixed}`;
|
||||
}
|
||||
if (needsClosing) {
|
||||
fixed = `${fixed}\n`;
|
||||
}
|
||||
return fixer.replaceText(node, fixed);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (option === 'never' && isParenthesized(context, node)) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
const tokenAfter = sourceCode.getTokenAfter(node);
|
||||
report(node, 'extraParens', (fixer) => fixer.replaceTextRange(
|
||||
[tokenBefore.range[0], tokenAfter.range[1]],
|
||||
getText(context, node)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
VariableDeclarator(node) {
|
||||
const type = 'declaration';
|
||||
if (!isEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
|
||||
check(node.init.consequent, type);
|
||||
check(node.init.alternate, type);
|
||||
return;
|
||||
}
|
||||
check(node.init, type);
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
const type = 'assignment';
|
||||
if (!isEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
|
||||
check(node.right.consequent, type);
|
||||
check(node.right.alternate, type);
|
||||
return;
|
||||
}
|
||||
check(node.right, type);
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
const type = 'return';
|
||||
if (isEnabled(type)) {
|
||||
check(node.argument, type);
|
||||
}
|
||||
},
|
||||
|
||||
'ArrowFunctionExpression:exit': (node) => {
|
||||
const arrowBody = node.body;
|
||||
const type = 'arrow';
|
||||
|
||||
if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
|
||||
check(arrowBody, type);
|
||||
}
|
||||
},
|
||||
|
||||
ConditionalExpression(node) {
|
||||
const type = 'condition';
|
||||
if (isEnabled(type)) {
|
||||
check(node.consequent, type);
|
||||
check(node.alternate, type);
|
||||
}
|
||||
},
|
||||
|
||||
LogicalExpression(node) {
|
||||
const type = 'logical';
|
||||
if (isEnabled(type)) {
|
||||
check(node.right, type);
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
const type = 'prop';
|
||||
if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
|
||||
check(node.value.expression, type);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
211
node_modules/eslint-plugin-react/lib/rules/no-access-state-in-setstate.js
generated
vendored
Normal file
211
node_modules/eslint-plugin-react/lib/rules/no-access-state-in-setstate.js
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of this.state within setState
|
||||
* @author Rolf Erik Lekang, Jørgen Aaberg
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const report = require('../util/report');
|
||||
const getScope = require('../util/eslint').getScope;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
useCallback: 'Use callback in setState when referencing the previous state.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow when this.state is accessed within setState',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-access-state-in-setstate'),
|
||||
},
|
||||
|
||||
messages,
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function isSetStateCall(node) {
|
||||
return astUtil.isCallExpression(node)
|
||||
&& node.callee.property
|
||||
&& node.callee.property.name === 'setState'
|
||||
&& node.callee.object.type === 'ThisExpression';
|
||||
}
|
||||
|
||||
function isFirstArgumentInSetStateCall(current, node) {
|
||||
if (!isSetStateCall(current)) {
|
||||
return false;
|
||||
}
|
||||
while (node && node.parent !== current) {
|
||||
node = node.parent;
|
||||
}
|
||||
return current.arguments[0] === node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isClassComponent(node) {
|
||||
return !!(
|
||||
componentUtil.getParentES6Component(context, node)
|
||||
|| componentUtil.getParentES5Component(context, node)
|
||||
);
|
||||
}
|
||||
|
||||
// The methods array contains all methods or functions that are using this.state
|
||||
// or that are calling another method or function using this.state
|
||||
const methods = [];
|
||||
// The vars array contains all variables that contains this.state
|
||||
const vars = [];
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (!isClassComponent(node)) {
|
||||
return;
|
||||
}
|
||||
// Appends all the methods that are calling another
|
||||
// method containing this.state to the methods array
|
||||
methods.forEach((method) => {
|
||||
if ('name' in node.callee && node.callee.name === method.methodName) {
|
||||
let current = node.parent;
|
||||
while (current.type !== 'Program') {
|
||||
if (current.type === 'MethodDefinition') {
|
||||
methods.push({
|
||||
methodName: 'name' in current.key ? current.key.name : undefined,
|
||||
node: method.node,
|
||||
});
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Finding all CallExpressions that is inside a setState
|
||||
// to further check if they contains this.state
|
||||
let current = node.parent;
|
||||
while (current.type !== 'Program') {
|
||||
if (isFirstArgumentInSetStateCall(current, node)) {
|
||||
const methodName = 'name' in node.callee ? node.callee.name : undefined;
|
||||
methods.forEach((method) => {
|
||||
if (method.methodName === methodName) {
|
||||
report(context, messages.useCallback, 'useCallback', {
|
||||
node: method.node,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
'name' in node.property
|
||||
&& node.property.name === 'state'
|
||||
&& node.object.type === 'ThisExpression'
|
||||
&& isClassComponent(node)
|
||||
) {
|
||||
/** @type {import("eslint").Rule.Node} */
|
||||
let current = node;
|
||||
while (current.type !== 'Program') {
|
||||
// Reporting if this.state is directly within this.setState
|
||||
if (isFirstArgumentInSetStateCall(current, node)) {
|
||||
report(context, messages.useCallback, 'useCallback', {
|
||||
node,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Storing all functions and methods that contains this.state
|
||||
if (current.type === 'MethodDefinition') {
|
||||
methods.push({
|
||||
methodName: 'name' in current.key ? current.key.name : undefined,
|
||||
node,
|
||||
});
|
||||
break;
|
||||
} else if (
|
||||
current.type === 'FunctionExpression'
|
||||
&& 'key' in current.parent
|
||||
&& current.parent.key
|
||||
) {
|
||||
methods.push({
|
||||
methodName: 'name' in current.parent.key ? current.parent.key.name : undefined,
|
||||
node,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Storing all variables containing this.state
|
||||
if (current.type === 'VariableDeclarator') {
|
||||
vars.push({
|
||||
node,
|
||||
scope: getScope(context, node),
|
||||
variableName: 'name' in current.id ? current.id.name : undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
// Checks if the identifier is a variable within an object
|
||||
/** @type {import("eslint").Rule.Node} */
|
||||
let current = node;
|
||||
while (current.parent.type === 'BinaryExpression') {
|
||||
current = current.parent;
|
||||
}
|
||||
if (
|
||||
('value' in current.parent && current.parent.value === current)
|
||||
|| ('object' in current.parent && current.parent.object === current)
|
||||
) {
|
||||
while (current.type !== 'Program') {
|
||||
if (isFirstArgumentInSetStateCall(current, node)) {
|
||||
vars
|
||||
.filter((v) => v.scope === getScope(context, node) && v.variableName === node.name)
|
||||
.forEach((v) => {
|
||||
report(context, messages.useCallback, 'useCallback', {
|
||||
node: v.node,
|
||||
});
|
||||
});
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ObjectPattern(node) {
|
||||
const isDerivedFromThis = 'init' in node.parent && node.parent.init && node.parent.init.type === 'ThisExpression';
|
||||
node.properties.forEach((property) => {
|
||||
if (
|
||||
property
|
||||
&& 'key' in property
|
||||
&& property.key
|
||||
&& 'name' in property.key
|
||||
&& property.key.name === 'state'
|
||||
&& isDerivedFromThis
|
||||
) {
|
||||
vars.push({
|
||||
node: property.key,
|
||||
scope: getScope(context, node),
|
||||
variableName: property.key.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
127
node_modules/eslint-plugin-react/lib/rules/no-adjacent-inline-elements.js
generated
vendored
Normal file
127
node_modules/eslint-plugin-react/lib/rules/no-adjacent-inline-elements.js
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @fileoverview Prevent adjacent inline elements not separated by whitespace.
|
||||
* @author Sean Hayes
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
const astUtil = require('../util/ast');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
|
||||
const inlineNames = [
|
||||
'a',
|
||||
'b',
|
||||
'big',
|
||||
'i',
|
||||
'small',
|
||||
'tt',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'cite',
|
||||
'code',
|
||||
'dfn',
|
||||
'em',
|
||||
'kbd',
|
||||
'strong',
|
||||
'samp',
|
||||
'time',
|
||||
'var',
|
||||
'bdo',
|
||||
'br',
|
||||
'img',
|
||||
'map',
|
||||
'object',
|
||||
'q',
|
||||
'script',
|
||||
'span',
|
||||
'sub',
|
||||
'sup',
|
||||
'button',
|
||||
'input',
|
||||
'label',
|
||||
'select',
|
||||
'textarea',
|
||||
];
|
||||
// Note: raw will be transformed into \u00a0.
|
||||
const whitespaceRegex = /(?:^\s|\s$)/;
|
||||
|
||||
function isInline(node) {
|
||||
if (node.type === 'Literal') {
|
||||
// Regular whitespace will be removed.
|
||||
const value = node.value;
|
||||
// To properly separate inline elements, each end of the literal will need
|
||||
// whitespace.
|
||||
return !whitespaceRegex.test(value);
|
||||
}
|
||||
if (node.type === 'JSXElement' && inlineNames.indexOf(node.openingElement.name.name) > -1) {
|
||||
return true;
|
||||
}
|
||||
if (astUtil.isCallExpression(node) && inlineNames.indexOf(node.arguments[0].value) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
inlineElement: 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow adjacent inline elements not separated by whitespace.',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-adjacent-inline-elements'),
|
||||
},
|
||||
schema: [],
|
||||
|
||||
messages,
|
||||
},
|
||||
create(context) {
|
||||
function validate(node, children) {
|
||||
let currentIsInline = false;
|
||||
let previousIsInline = false;
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
currentIsInline = isInline(children[i]);
|
||||
if (previousIsInline && currentIsInline) {
|
||||
report(context, messages.inlineElement, 'inlineElement', {
|
||||
node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
previousIsInline = currentIsInline;
|
||||
}
|
||||
}
|
||||
return {
|
||||
JSXElement(node) {
|
||||
validate(node, node.children);
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElement(context, node)) {
|
||||
return;
|
||||
}
|
||||
if (node.arguments.length < 2 || !node.arguments[2]) {
|
||||
return;
|
||||
}
|
||||
const children = 'elements' in node.arguments[2] ? node.arguments[2].elements : undefined;
|
||||
validate(node, children);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
293
node_modules/eslint-plugin-react/lib/rules/no-array-index-key.js
generated
vendored
Normal file
293
node_modules/eslint-plugin-react/lib/rules/no-array-index-key.js
generated
vendored
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of Array index in keys
|
||||
* @author Joe Lencioni
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const pragma = require('../util/pragma');
|
||||
const report = require('../util/report');
|
||||
const variableUtil = require('../util/variable');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function isCreateCloneElement(node, context) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
|
||||
return node.object
|
||||
&& node.object.name === pragma.getFromContext(context)
|
||||
&& ['createElement', 'cloneElement'].indexOf(node.property.name) !== -1;
|
||||
}
|
||||
|
||||
if (node.type === 'Identifier') {
|
||||
const variable = variableUtil.findVariableByName(context, node, node.name);
|
||||
if (variable && variable.type === 'ImportSpecifier') {
|
||||
return variable.parent.source.value === 'react';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
noArrayIndex: 'Do not use Array index in keys',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of Array index in keys',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-array-index-key'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
const indexParamNames = [];
|
||||
const iteratorFunctionsToIndexParamPosition = {
|
||||
every: 1,
|
||||
filter: 1,
|
||||
find: 1,
|
||||
findIndex: 1,
|
||||
flatMap: 1,
|
||||
forEach: 1,
|
||||
map: 1,
|
||||
reduce: 2,
|
||||
reduceRight: 2,
|
||||
some: 1,
|
||||
};
|
||||
|
||||
function isArrayIndex(node) {
|
||||
return node.type === 'Identifier'
|
||||
&& indexParamNames.indexOf(node.name) !== -1;
|
||||
}
|
||||
|
||||
function isUsingReactChildren(node) {
|
||||
const callee = node.callee;
|
||||
if (
|
||||
!callee
|
||||
|| !callee.property
|
||||
|| !callee.object
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
|
||||
if (!isReactChildMethod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const obj = callee.object;
|
||||
if (obj && obj.name === 'Children') {
|
||||
return true;
|
||||
}
|
||||
if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMapIndexParamName(node) {
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') {
|
||||
return null;
|
||||
}
|
||||
if (callee.property.type !== 'Identifier') {
|
||||
return null;
|
||||
}
|
||||
if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = /** @type {keyof iteratorFunctionsToIndexParamPosition} */ (callee.property.name);
|
||||
|
||||
const callbackArg = isUsingReactChildren(node)
|
||||
? node.arguments[1]
|
||||
: node.arguments[0];
|
||||
|
||||
if (!callbackArg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!astUtil.isFunctionLikeExpression(callbackArg)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = callbackArg.params;
|
||||
|
||||
const indexParamPosition = iteratorFunctionsToIndexParamPosition[name];
|
||||
if (params.length < indexParamPosition + 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return params[indexParamPosition].name;
|
||||
}
|
||||
|
||||
function getIdentifiersFromBinaryExpression(side) {
|
||||
if (side.type === 'Identifier') {
|
||||
return side;
|
||||
}
|
||||
|
||||
if (side.type === 'BinaryExpression') {
|
||||
// recurse
|
||||
const left = getIdentifiersFromBinaryExpression(side.left);
|
||||
const right = getIdentifiersFromBinaryExpression(side.right);
|
||||
return [].concat(left, right).filter(Boolean);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkPropValue(node) {
|
||||
if (isArrayIndex(node)) {
|
||||
// key={bar}
|
||||
report(context, messages.noArrayIndex, 'noArrayIndex', {
|
||||
node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'TemplateLiteral') {
|
||||
// key={`foo-${bar}`}
|
||||
node.expressions.filter(isArrayIndex).forEach(() => {
|
||||
report(context, messages.noArrayIndex, 'noArrayIndex', {
|
||||
node,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'BinaryExpression') {
|
||||
// key={'foo' + bar}
|
||||
const identifiers = getIdentifiersFromBinaryExpression(node);
|
||||
|
||||
identifiers.filter(isArrayIndex).forEach(() => {
|
||||
report(context, messages.noArrayIndex, 'noArrayIndex', {
|
||||
node,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
astUtil.isCallExpression(node)
|
||||
&& node.callee
|
||||
&& node.callee.type === 'MemberExpression'
|
||||
&& node.callee.object
|
||||
&& isArrayIndex(node.callee.object)
|
||||
&& node.callee.property
|
||||
&& node.callee.property.type === 'Identifier'
|
||||
&& node.callee.property.name === 'toString'
|
||||
) {
|
||||
// key={bar.toString()}
|
||||
report(context, messages.noArrayIndex, 'noArrayIndex', {
|
||||
node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
astUtil.isCallExpression(node)
|
||||
&& node.callee
|
||||
&& node.callee.type === 'Identifier'
|
||||
&& node.callee.name === 'String'
|
||||
&& Array.isArray(node.arguments)
|
||||
&& node.arguments.length > 0
|
||||
&& isArrayIndex(node.arguments[0])
|
||||
) {
|
||||
// key={String(bar)}
|
||||
report(context, messages.noArrayIndex, 'noArrayIndex', {
|
||||
node: node.arguments[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function popIndex(node) {
|
||||
const mapIndexParamName = getMapIndexParamName(node);
|
||||
if (!mapIndexParamName) {
|
||||
return;
|
||||
}
|
||||
|
||||
indexParamNames.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
'CallExpression, OptionalCallExpression'(node) {
|
||||
if (isCreateCloneElement(node.callee, context) && node.arguments.length > 1) {
|
||||
// React.createElement
|
||||
if (!indexParamNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const props = node.arguments[1];
|
||||
|
||||
if (props.type !== 'ObjectExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
props.properties.forEach((prop) => {
|
||||
if (!prop.key || prop.key.name !== 'key') {
|
||||
// { ...foo }
|
||||
// { foo: bar }
|
||||
return;
|
||||
}
|
||||
|
||||
checkPropValue(prop.value);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const mapIndexParamName = getMapIndexParamName(node);
|
||||
if (!mapIndexParamName) {
|
||||
return;
|
||||
}
|
||||
|
||||
indexParamNames.push(mapIndexParamName);
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
if (node.name.name !== 'key') {
|
||||
// foo={bar}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indexParamNames.length) {
|
||||
// Not inside a call expression that we think has an index param.
|
||||
return;
|
||||
}
|
||||
|
||||
const value = node.value;
|
||||
if (!value || value.type !== 'JSXExpressionContainer') {
|
||||
// key='foo' or just simply 'key'
|
||||
return;
|
||||
}
|
||||
|
||||
checkPropValue(value.expression);
|
||||
},
|
||||
|
||||
'CallExpression:exit': popIndex,
|
||||
'OptionalCallExpression:exit': popIndex,
|
||||
};
|
||||
},
|
||||
};
|
||||
149
node_modules/eslint-plugin-react/lib/rules/no-arrow-function-lifecycle.js
generated
vendored
Normal file
149
node_modules/eslint-plugin-react/lib/rules/no-arrow-function-lifecycle.js
generated
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @fileoverview Lifecycle methods should be methods on the prototype, not class fields
|
||||
* @author Tan Nguyen
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const lifecycleMethods = require('../util/lifecycleMethods');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
function getRuleText(node) {
|
||||
const params = node.value.params.map((p) => p.name);
|
||||
|
||||
if (node.type === 'Property') {
|
||||
return `: function(${params.join(', ')}) `;
|
||||
}
|
||||
|
||||
if (node.type === 'ClassProperty' || node.type === 'PropertyDefinition') {
|
||||
return `(${params.join(', ')}) `;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
lifecycle: '{{propertyName}} is a React lifecycle method, and should not be an arrow function or in a class field. Use an instance method instead.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Lifecycle methods should be methods on the prototype, not class fields',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-arrow-function-lifecycle'),
|
||||
},
|
||||
messages,
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
/**
|
||||
* @param {Array} properties list of component properties
|
||||
*/
|
||||
function reportNoArrowFunctionLifecycle(properties) {
|
||||
properties.forEach((node) => {
|
||||
if (!node || !node.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyName = astUtil.getPropertyName(node);
|
||||
const nodeType = node.value.type;
|
||||
const isLifecycleMethod = (
|
||||
node.static && !componentUtil.isES5Component(node, context)
|
||||
? lifecycleMethods.static
|
||||
: lifecycleMethods.instance
|
||||
).indexOf(propertyName) > -1;
|
||||
|
||||
if (nodeType === 'ArrowFunctionExpression' && isLifecycleMethod) {
|
||||
const body = node.value.body;
|
||||
const isBlockBody = body.type === 'BlockStatement';
|
||||
const sourceCode = getSourceCode(context);
|
||||
|
||||
let nextComment = [];
|
||||
let previousComment = [];
|
||||
let bodyRange;
|
||||
if (!isBlockBody) {
|
||||
const previousToken = sourceCode.getTokenBefore(body);
|
||||
|
||||
if (sourceCode.getCommentsBefore) {
|
||||
// eslint >=4.x
|
||||
previousComment = sourceCode.getCommentsBefore(body);
|
||||
} else {
|
||||
// eslint 3.x
|
||||
const potentialComment = sourceCode.getTokenBefore(body, { includeComments: true });
|
||||
previousComment = previousToken === potentialComment ? [] : [potentialComment];
|
||||
}
|
||||
|
||||
if (sourceCode.getCommentsAfter) {
|
||||
// eslint >=4.x
|
||||
nextComment = sourceCode.getCommentsAfter(body);
|
||||
} else {
|
||||
// eslint 3.x
|
||||
const potentialComment = sourceCode.getTokenAfter(body, { includeComments: true });
|
||||
const nextToken = sourceCode.getTokenAfter(body);
|
||||
nextComment = nextToken === potentialComment ? [] : [potentialComment];
|
||||
}
|
||||
bodyRange = [
|
||||
(previousComment.length > 0 ? previousComment[0] : body).range[0],
|
||||
(nextComment.length > 0 ? nextComment[nextComment.length - 1] : body).range[1]
|
||||
+ (node.value.body.type === 'ObjectExpression' ? 1 : 0), // to account for a wrapped end paren
|
||||
];
|
||||
}
|
||||
const headRange = [
|
||||
node.key.range[1],
|
||||
(previousComment.length > 0 ? previousComment[0] : body).range[0],
|
||||
];
|
||||
const hasSemi = node.value.expression && getText(context, node).slice(node.value.range[1] - node.range[0]) === ';';
|
||||
|
||||
report(
|
||||
context,
|
||||
messages.lifecycle,
|
||||
'lifecycle',
|
||||
{
|
||||
node,
|
||||
data: {
|
||||
propertyName,
|
||||
},
|
||||
fix(fixer) {
|
||||
if (!sourceCode.getCommentsAfter) {
|
||||
// eslint 3.x
|
||||
return isBlockBody && fixer.replaceTextRange(headRange, getRuleText(node));
|
||||
}
|
||||
return [].concat(
|
||||
fixer.replaceTextRange(headRange, getRuleText(node)),
|
||||
isBlockBody ? [] : fixer.replaceTextRange(
|
||||
[bodyRange[0], bodyRange[1] + (hasSemi ? 1 : 0)],
|
||||
`{ return ${previousComment.map((x) => getText(context, x)).join('')}${getText(context, body)}${nextComment.map((x) => getText(context, x)).join('')}; }`
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
values(components.list()).forEach((component) => {
|
||||
const properties = astUtil.getComponentProperties(component.node);
|
||||
reportNoArrowFunctionLifecycle(properties);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
125
node_modules/eslint-plugin-react/lib/rules/no-children-prop.js
generated
vendored
Normal file
125
node_modules/eslint-plugin-react/lib/rules/no-children-prop.js
generated
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @fileoverview Prevent passing of children as props
|
||||
* @author Benjamin Stepp
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if the node is a createElement call with a props literal.
|
||||
* @param {ASTNode} node - The AST node being checked.
|
||||
* @param {Context} context - The AST node being checked.
|
||||
* @returns {boolean} - True if node is a createElement call with a props
|
||||
* object literal, False if not.
|
||||
*/
|
||||
function isCreateElementWithProps(node, context) {
|
||||
return isCreateElement(context, node)
|
||||
&& node.arguments.length > 1
|
||||
&& node.arguments[1].type === 'ObjectExpression';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
nestChildren: 'Do not pass children as props. Instead, nest children between the opening and closing tags.',
|
||||
passChildrenAsArgs: 'Do not pass children as props. Instead, pass them as additional arguments to React.createElement.',
|
||||
nestFunction: 'Do not nest a function between the opening and closing tags. Instead, pass it as a prop.',
|
||||
passFunctionAsArgs: 'Do not pass a function as an additional argument to React.createElement. Instead, pass it as a prop.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow passing of children as props',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-children-prop'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowFunctions: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
|
||||
function isFunction(node) {
|
||||
return configuration.allowFunctions && (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression');
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (node.name.name !== 'children') {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = node.value;
|
||||
if (value && value.type === 'JSXExpressionContainer' && isFunction(value.expression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.nestChildren, 'nestChildren', {
|
||||
node,
|
||||
});
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isCreateElementWithProps(node, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const props = 'properties' in node.arguments[1] ? node.arguments[1].properties : undefined;
|
||||
const childrenProp = props.find((prop) => (
|
||||
'key' in prop
|
||||
&& prop.key
|
||||
&& 'name' in prop.key
|
||||
&& prop.key.name === 'children'
|
||||
));
|
||||
|
||||
if (childrenProp) {
|
||||
if ('value' in childrenProp && childrenProp.value && !isFunction(childrenProp.value)) {
|
||||
report(context, messages.passChildrenAsArgs, 'passChildrenAsArgs', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
} else if (node.arguments.length === 3) {
|
||||
const children = node.arguments[2];
|
||||
if (isFunction(children)) {
|
||||
report(context, messages.passFunctionAsArgs, 'passFunctionAsArgs', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
JSXElement(node) {
|
||||
const children = node.children;
|
||||
if (children && children.length === 1 && children[0].type === 'JSXExpressionContainer') {
|
||||
if (isFunction(children[0].expression)) {
|
||||
report(context, messages.nestFunction, 'nestFunction', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
157
node_modules/eslint-plugin-react/lib/rules/no-danger-with-children.js
generated
vendored
Normal file
157
node_modules/eslint-plugin-react/lib/rules/no-danger-with-children.js
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @fileoverview Report when a DOM element is using both children and dangerouslySetInnerHTML
|
||||
* @author David Petersen
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
const messages = {
|
||||
dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow when a DOM element is using both children and dangerouslySetInnerHTML',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('no-danger-with-children'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [], // no options
|
||||
},
|
||||
create(context) {
|
||||
function findSpreadVariable(node, name) {
|
||||
return variableUtil.getVariableFromContext(context, node, name);
|
||||
}
|
||||
/**
|
||||
* Takes a ObjectExpression and returns the value of the prop if it has it
|
||||
* @param {object} node - ObjectExpression node
|
||||
* @param {string} propName - name of the prop to look for
|
||||
* @param {string[]} seenProps
|
||||
* @returns {object | boolean}
|
||||
*/
|
||||
function findObjectProp(node, propName, seenProps) {
|
||||
if (!node.properties) {
|
||||
return false;
|
||||
}
|
||||
return node.properties.find((prop) => {
|
||||
if (prop.type === 'Property') {
|
||||
return prop.key.name === propName;
|
||||
}
|
||||
if (prop.type === 'ExperimentalSpreadProperty' || prop.type === 'SpreadElement') {
|
||||
const variable = findSpreadVariable(node, prop.argument.name);
|
||||
if (variable && variable.defs.length && variable.defs[0].node.init) {
|
||||
if (seenProps.indexOf(prop.argument.name) > -1) {
|
||||
return false;
|
||||
}
|
||||
const newSeenProps = seenProps.concat(prop.argument.name || []);
|
||||
return findObjectProp(variable.defs[0].node.init, propName, newSeenProps);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a JSXElement and returns the value of the prop if it has it
|
||||
* @param {object} node - JSXElement node
|
||||
* @param {string} propName - name of the prop to look for
|
||||
* @returns {object | boolean}
|
||||
*/
|
||||
function findJsxProp(node, propName) {
|
||||
const attributes = node.openingElement.attributes;
|
||||
return attributes.find((attribute) => {
|
||||
if (attribute.type === 'JSXSpreadAttribute') {
|
||||
const variable = findSpreadVariable(node, attribute.argument.name);
|
||||
if (variable && variable.defs.length && variable.defs[0].node.init) {
|
||||
return findObjectProp(variable.defs[0].node.init, propName, []);
|
||||
}
|
||||
}
|
||||
return attribute.name && attribute.name.name === propName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a node is a line break
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {boolean} True if node is a line break, false if not
|
||||
*/
|
||||
function isLineBreak(node) {
|
||||
const isLiteral = node.type === 'Literal' || node.type === 'JSXText';
|
||||
const isMultiline = node.loc.start.line !== node.loc.end.line;
|
||||
const isWhiteSpaces = jsxUtil.isWhiteSpaces(node.value);
|
||||
|
||||
return isLiteral && isMultiline && isWhiteSpaces;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXElement(node) {
|
||||
let hasChildren = false;
|
||||
|
||||
if (node.children.length && !isLineBreak(node.children[0])) {
|
||||
hasChildren = true;
|
||||
} else if (findJsxProp(node, 'children')) {
|
||||
hasChildren = true;
|
||||
}
|
||||
|
||||
if (
|
||||
node.openingElement.attributes
|
||||
&& hasChildren
|
||||
&& findJsxProp(node, 'dangerouslySetInnerHTML')
|
||||
) {
|
||||
report(context, messages.dangerWithChildren, 'dangerWithChildren', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee
|
||||
&& node.callee.type === 'MemberExpression'
|
||||
&& 'name' in node.callee.property
|
||||
&& node.callee.property.name === 'createElement'
|
||||
&& node.arguments.length > 1
|
||||
) {
|
||||
let hasChildren = false;
|
||||
|
||||
let props = node.arguments[1];
|
||||
|
||||
if (props.type === 'Identifier') {
|
||||
const variable = variableUtil.getVariableFromContext(context, node, props.name);
|
||||
if (variable && variable.defs.length && variable.defs[0].node.init) {
|
||||
props = variable.defs[0].node.init;
|
||||
}
|
||||
}
|
||||
|
||||
const dangerously = findObjectProp(props, 'dangerouslySetInnerHTML', []);
|
||||
|
||||
if (node.arguments.length === 2) {
|
||||
if (findObjectProp(props, 'children', [])) {
|
||||
hasChildren = true;
|
||||
}
|
||||
} else {
|
||||
hasChildren = true;
|
||||
}
|
||||
|
||||
if (dangerously && hasChildren) {
|
||||
report(context, messages.dangerWithChildren, 'dangerWithChildren', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
95
node_modules/eslint-plugin-react/lib/rules/no-danger.js
generated
vendored
Normal file
95
node_modules/eslint-plugin-react/lib/rules/no-danger.js
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of dangerous JSX props
|
||||
* @author Scott Andrews
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const fromEntries = require('object.fromentries/polyfill')();
|
||||
const minimatch = require('minimatch');
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DANGEROUS_PROPERTY_NAMES = [
|
||||
'dangerouslySetInnerHTML',
|
||||
];
|
||||
|
||||
const DANGEROUS_PROPERTIES = fromEntries(DANGEROUS_PROPERTY_NAMES.map((prop) => [prop, prop]));
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if a JSX attribute is dangerous.
|
||||
* @param {string} name - Name of the attribute to check.
|
||||
* @returns {boolean} Whether or not the attribute is dangerous.
|
||||
*/
|
||||
function isDangerous(name) {
|
||||
return has(DANGEROUS_PROPERTIES, name);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
dangerousProp: 'Dangerous property \'{{name}}\' found',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of dangerous JSX properties',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-danger'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
customComponentNames: {
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
minItems: 0,
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const customComponentNames = configuration.customComponentNames || [];
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const functionName = node.parent.name.name;
|
||||
|
||||
const enableCheckingCustomComponent = customComponentNames.some((name) => minimatch(functionName, name));
|
||||
|
||||
if ((enableCheckingCustomComponent || jsxUtil.isDOMComponent(node.parent)) && isDangerous(node.name.name)) {
|
||||
report(context, messages.dangerousProp, 'dangerousProp', {
|
||||
node,
|
||||
data: {
|
||||
name: node.name.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
271
node_modules/eslint-plugin-react/lib/rules/no-deprecated.js
generated
vendored
Normal file
271
node_modules/eslint-plugin-react/lib/rules/no-deprecated.js
generated
vendored
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of deprecated methods
|
||||
* @author Yannick Croissant
|
||||
* @author Scott Feeney
|
||||
* @author Sergei Startsev
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const entries = require('object.entries');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const MODULES = {
|
||||
react: ['React'],
|
||||
'react-addons-perf': ['ReactPerf', 'Perf'],
|
||||
'react-dom': ['ReactDOM'],
|
||||
'react-dom/server': ['ReactDOMServer'],
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
function getDeprecated(pragma) {
|
||||
const deprecated = {};
|
||||
// 0.12.0
|
||||
deprecated[`${pragma}.renderComponent`] = ['0.12.0', `${pragma}.render`];
|
||||
deprecated[`${pragma}.renderComponentToString`] = ['0.12.0', `${pragma}.renderToString`];
|
||||
deprecated[`${pragma}.renderComponentToStaticMarkup`] = ['0.12.0', `${pragma}.renderToStaticMarkup`];
|
||||
deprecated[`${pragma}.isValidComponent`] = ['0.12.0', `${pragma}.isValidElement`];
|
||||
deprecated[`${pragma}.PropTypes.component`] = ['0.12.0', `${pragma}.PropTypes.element`];
|
||||
deprecated[`${pragma}.PropTypes.renderable`] = ['0.12.0', `${pragma}.PropTypes.node`];
|
||||
deprecated[`${pragma}.isValidClass`] = ['0.12.0'];
|
||||
deprecated['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})'];
|
||||
// 0.13.0
|
||||
deprecated[`${pragma}.addons.classSet`] = ['0.13.0', 'the npm module classnames'];
|
||||
deprecated[`${pragma}.addons.cloneWithProps`] = ['0.13.0', `${pragma}.cloneElement`];
|
||||
// 0.14.0
|
||||
deprecated[`${pragma}.render`] = ['0.14.0', 'ReactDOM.render'];
|
||||
deprecated[`${pragma}.unmountComponentAtNode`] = ['0.14.0', 'ReactDOM.unmountComponentAtNode'];
|
||||
deprecated[`${pragma}.findDOMNode`] = ['0.14.0', 'ReactDOM.findDOMNode'];
|
||||
deprecated[`${pragma}.renderToString`] = ['0.14.0', 'ReactDOMServer.renderToString'];
|
||||
deprecated[`${pragma}.renderToStaticMarkup`] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup'];
|
||||
// 15.0.0
|
||||
deprecated[`${pragma}.addons.LinkedStateMixin`] = ['15.0.0'];
|
||||
deprecated['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations'];
|
||||
deprecated['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations'];
|
||||
deprecated['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted'];
|
||||
deprecated['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted'];
|
||||
// 15.5.0
|
||||
deprecated[`${pragma}.createClass`] = ['15.5.0', 'the npm module create-react-class'];
|
||||
deprecated[`${pragma}.addons.TestUtils`] = ['15.5.0', 'ReactDOM.TestUtils'];
|
||||
deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types'];
|
||||
// 15.6.0
|
||||
deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories'];
|
||||
// 16.9.0
|
||||
// For now the following life-cycle methods are just legacy, not deprecated:
|
||||
// `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate`
|
||||
// https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934
|
||||
deprecated.componentWillMount = [
|
||||
'16.9.0',
|
||||
'UNSAFE_componentWillMount',
|
||||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. '
|
||||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
|
||||
];
|
||||
deprecated.componentWillReceiveProps = [
|
||||
'16.9.0',
|
||||
'UNSAFE_componentWillReceiveProps',
|
||||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. '
|
||||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
|
||||
];
|
||||
deprecated.componentWillUpdate = [
|
||||
'16.9.0',
|
||||
'UNSAFE_componentWillUpdate',
|
||||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. '
|
||||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
|
||||
];
|
||||
// 18.0.0
|
||||
// https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#deprecations
|
||||
deprecated['ReactDOM.render'] = [
|
||||
'18.0.0',
|
||||
'createRoot',
|
||||
'https://reactjs.org/link/switch-to-createroot',
|
||||
];
|
||||
deprecated['ReactDOM.hydrate'] = [
|
||||
'18.0.0',
|
||||
'hydrateRoot',
|
||||
'https://reactjs.org/link/switch-to-createroot',
|
||||
];
|
||||
deprecated['ReactDOM.unmountComponentAtNode'] = [
|
||||
'18.0.0',
|
||||
'root.unmount',
|
||||
'https://reactjs.org/link/switch-to-createroot',
|
||||
];
|
||||
deprecated['ReactDOMServer.renderToNodeStream'] = [
|
||||
'18.0.0',
|
||||
'renderToPipeableStream',
|
||||
'https://reactjs.org/docs/react-dom-server.html#rendertonodestream',
|
||||
];
|
||||
|
||||
return deprecated;
|
||||
}
|
||||
|
||||
const messages = {
|
||||
deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of deprecated methods',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-deprecated'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
const deprecated = getDeprecated(pragma);
|
||||
|
||||
function isDeprecated(method) {
|
||||
return (
|
||||
deprecated
|
||||
&& deprecated[method]
|
||||
&& deprecated[method][0]
|
||||
&& testReactVersion(context, `>= ${deprecated[method][0]}`)
|
||||
);
|
||||
}
|
||||
|
||||
function checkDeprecation(node, methodName, methodNode) {
|
||||
if (!isDeprecated(methodName)) {
|
||||
return;
|
||||
}
|
||||
const version = deprecated[methodName][0];
|
||||
const newMethod = deprecated[methodName][1];
|
||||
const refs = deprecated[methodName][2];
|
||||
report(context, messages.deprecated, 'deprecated', {
|
||||
node: methodNode || node,
|
||||
data: {
|
||||
oldMethod: methodName,
|
||||
version,
|
||||
newMethod: newMethod ? `, use ${newMethod} instead` : '',
|
||||
refs: refs ? `, see ${refs}` : '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getReactModuleName(node) {
|
||||
let moduleName = false;
|
||||
if (!node.init) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entries(MODULES).some((entry) => {
|
||||
const key = entry[0];
|
||||
const moduleNames = entry[1];
|
||||
if (
|
||||
node.init.arguments
|
||||
&& node.init.arguments.length > 0
|
||||
&& node.init.arguments[0]
|
||||
&& key === node.init.arguments[0].value
|
||||
) {
|
||||
moduleName = MODULES[key][0];
|
||||
} else {
|
||||
moduleName = moduleNames.find((name) => name === node.init.name);
|
||||
}
|
||||
return moduleName;
|
||||
});
|
||||
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns life cycle methods if available
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Array} The array of methods.
|
||||
*/
|
||||
function getLifeCycleMethods(node) {
|
||||
const properties = astUtil.getComponentProperties(node);
|
||||
return properties.map((property) => ({
|
||||
name: astUtil.getPropertyName(property),
|
||||
node: astUtil.getPropertyNameNode(property),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks life cycle methods
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function checkLifeCycleMethods(node) {
|
||||
if (
|
||||
componentUtil.isES5Component(node, context)
|
||||
|| componentUtil.isES6Component(node, context)
|
||||
) {
|
||||
const methods = getLifeCycleMethods(node);
|
||||
methods.forEach((method) => checkDeprecation(node, method.name, method.node));
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
checkDeprecation(node, getText(context, node));
|
||||
},
|
||||
|
||||
ImportDeclaration(node) {
|
||||
const isReactImport = typeof MODULES[node.source.value] !== 'undefined';
|
||||
if (!isReactImport) {
|
||||
return;
|
||||
}
|
||||
node.specifiers.filter(((s) => 'imported' in s && s.imported)).forEach((specifier) => {
|
||||
// TODO, semver-major: remove `in` check as part of jsdoc->tsdoc migration
|
||||
checkDeprecation(node, 'imported' in specifier && `${MODULES[node.source.value][0]}.${specifier.imported.name}`, specifier);
|
||||
});
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
const reactModuleName = getReactModuleName(node);
|
||||
const isRequire = node.init
|
||||
&& 'callee' in node.init
|
||||
&& node.init.callee
|
||||
&& 'name' in node.init.callee
|
||||
&& node.init.callee.name === 'require';
|
||||
const isReactRequire = node.init
|
||||
&& 'arguments' in node.init
|
||||
&& node.init.arguments
|
||||
&& node.init.arguments.length
|
||||
&& typeof MODULES['value' in node.init.arguments[0] ? node.init.arguments[0].value : undefined] !== 'undefined';
|
||||
const isDestructuring = node.id && node.id.type === 'ObjectPattern';
|
||||
|
||||
if (
|
||||
!(isDestructuring && reactModuleName)
|
||||
&& !(isDestructuring && isRequire && isReactRequire)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
('properties' in node.id ? node.id.properties : undefined).filter((p) => p.type !== 'RestElement' && p.key).forEach((property) => {
|
||||
checkDeprecation(
|
||||
node,
|
||||
'key' in property && 'name' in property.key && `${reactModuleName || pragma}.${property.key.name}`,
|
||||
property
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
ClassDeclaration: checkLifeCycleMethods,
|
||||
ClassExpression: checkLifeCycleMethods,
|
||||
ObjectExpression: checkLifeCycleMethods,
|
||||
};
|
||||
},
|
||||
};
|
||||
11
node_modules/eslint-plugin-react/lib/rules/no-did-mount-set-state.js
generated
vendored
Normal file
11
node_modules/eslint-plugin-react/lib/rules/no-did-mount-set-state.js
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState in componentDidMount
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = makeNoMethodSetStateRule('componentDidMount');
|
||||
11
node_modules/eslint-plugin-react/lib/rules/no-did-update-set-state.js
generated
vendored
Normal file
11
node_modules/eslint-plugin-react/lib/rules/no-did-update-set-state.js
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState in componentDidUpdate
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = makeNoMethodSetStateRule('componentDidUpdate');
|
||||
155
node_modules/eslint-plugin-react/lib/rules/no-direct-mutation-state.js
generated
vendored
Normal file
155
node_modules/eslint-plugin-react/lib/rules/no-direct-mutation-state.js
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @fileoverview Prevent direct mutation of this.state
|
||||
* @author David Petersen
|
||||
* @author Nicolas Fernandez <@burabure>
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noDirectMutation: 'Do not mutate state directly. Use setState().',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow direct mutation of this.state',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('no-direct-mutation-state'),
|
||||
},
|
||||
|
||||
messages,
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
/**
|
||||
* Checks if the component is valid
|
||||
* @param {Object} component The component to process
|
||||
* @returns {boolean} True if the component is valid, false if not.
|
||||
*/
|
||||
function isValid(component) {
|
||||
return !!component && !component.mutateSetState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports undeclared proptypes for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportMutations(component) {
|
||||
let mutation;
|
||||
for (let i = 0, j = component.mutations.length; i < j; i++) {
|
||||
mutation = component.mutations[i];
|
||||
report(context, messages.noDirectMutation, 'noDirectMutation', {
|
||||
node: mutation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through the MemberExpression to the top-most property.
|
||||
* @param {Object} node The node to process
|
||||
* @returns {Object} The outer-most MemberExpression
|
||||
*/
|
||||
function getOuterMemberExpression(node) {
|
||||
while (node.object && node.object.property) {
|
||||
node = node.object;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should currently ignore assignments in this component.
|
||||
* @param {?Object} component The component to process
|
||||
* @returns {boolean} True if we should skip assignment checks.
|
||||
*/
|
||||
function shouldIgnoreComponent(component) {
|
||||
return !component || (component.inConstructor && !component.inCallExpression);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
return {
|
||||
MethodDefinition(node) {
|
||||
if (node.kind === 'constructor') {
|
||||
components.set(node, {
|
||||
inConstructor: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
components.set(node, {
|
||||
inCallExpression: true,
|
||||
});
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
const component = components.get(utils.getParentComponent(node));
|
||||
if (shouldIgnoreComponent(component) || !node.left || !node.left.object) {
|
||||
return;
|
||||
}
|
||||
const item = getOuterMemberExpression(node.left);
|
||||
if (componentUtil.isStateMemberExpression(item)) {
|
||||
const mutations = (component && component.mutations) || [];
|
||||
mutations.push(node.left.object);
|
||||
components.set(node, {
|
||||
mutateSetState: true,
|
||||
mutations,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
UpdateExpression(node) {
|
||||
const component = components.get(utils.getParentComponent(node));
|
||||
if (shouldIgnoreComponent(component) || node.argument.type !== 'MemberExpression') {
|
||||
return;
|
||||
}
|
||||
const item = getOuterMemberExpression(node.argument);
|
||||
if (componentUtil.isStateMemberExpression(item)) {
|
||||
const mutations = (component && component.mutations) || [];
|
||||
mutations.push(item);
|
||||
components.set(node, {
|
||||
mutateSetState: true,
|
||||
mutations,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'CallExpression:exit'(node) {
|
||||
components.set(node, {
|
||||
inCallExpression: false,
|
||||
});
|
||||
},
|
||||
|
||||
'MethodDefinition:exit'(node) {
|
||||
if (node.kind === 'constructor') {
|
||||
components.set(node, {
|
||||
inConstructor: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
values(components.list())
|
||||
.filter((component) => !isValid(component))
|
||||
.forEach((component) => {
|
||||
reportMutations(component);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
56
node_modules/eslint-plugin-react/lib/rules/no-find-dom-node.js
generated
vendored
Normal file
56
node_modules/eslint-plugin-react/lib/rules/no-find-dom-node.js
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of findDOMNode
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noFindDOMNode: 'Do not use findDOMNode. It doesn’t work with function components and is deprecated in StrictMode. See https://reactjs.org/docs/react-dom.html#finddomnode',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of findDOMNode',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-find-dom-node'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
|
||||
const isFindDOMNode = ('name' in callee && callee.name === 'findDOMNode') || (
|
||||
'property' in callee
|
||||
&& callee.property
|
||||
&& 'name' in callee.property
|
||||
&& callee.property.name === 'findDOMNode'
|
||||
);
|
||||
|
||||
if (!isFindDOMNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.noFindDOMNode, 'noFindDOMNode', {
|
||||
node: callee,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
654
node_modules/eslint-plugin-react/lib/rules/no-invalid-html-attribute.js
generated
vendored
Normal file
654
node_modules/eslint-plugin-react/lib/rules/no-invalid-html-attribute.js
generated
vendored
Normal file
@@ -0,0 +1,654 @@
|
||||
/**
|
||||
* @fileoverview Check if tag attributes to have non-valid value
|
||||
* @author Sebastian Malton
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const matchAll = require('string.prototype.matchall');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const rel = new Map([
|
||||
['alternate', new Set(['link', 'area', 'a'])],
|
||||
['apple-touch-icon', new Set(['link'])],
|
||||
['apple-touch-startup-image', new Set(['link'])],
|
||||
['author', new Set(['link', 'area', 'a'])],
|
||||
['bookmark', new Set(['area', 'a'])],
|
||||
['canonical', new Set(['link'])],
|
||||
['dns-prefetch', new Set(['link'])],
|
||||
['external', new Set(['area', 'a', 'form'])],
|
||||
['help', new Set(['link', 'area', 'a', 'form'])],
|
||||
['icon', new Set(['link'])],
|
||||
['license', new Set(['link', 'area', 'a', 'form'])],
|
||||
['manifest', new Set(['link'])],
|
||||
['mask-icon', new Set(['link'])],
|
||||
['modulepreload', new Set(['link'])],
|
||||
['next', new Set(['link', 'area', 'a', 'form'])],
|
||||
['nofollow', new Set(['area', 'a', 'form'])],
|
||||
['noopener', new Set(['area', 'a', 'form'])],
|
||||
['noreferrer', new Set(['area', 'a', 'form'])],
|
||||
['opener', new Set(['area', 'a', 'form'])],
|
||||
['pingback', new Set(['link'])],
|
||||
['preconnect', new Set(['link'])],
|
||||
['prefetch', new Set(['link'])],
|
||||
['preload', new Set(['link'])],
|
||||
['prerender', new Set(['link'])],
|
||||
['prev', new Set(['link', 'area', 'a', 'form'])],
|
||||
['search', new Set(['link', 'area', 'a', 'form'])],
|
||||
['shortcut', new Set(['link'])], // generally allowed but needs pair with "icon"
|
||||
['shortcut\u0020icon', new Set(['link'])],
|
||||
['stylesheet', new Set(['link'])],
|
||||
['tag', new Set(['area', 'a'])],
|
||||
]);
|
||||
|
||||
const pairs = new Map([
|
||||
['shortcut', new Set(['icon'])],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Map between attributes and a mapping between valid values and a set of tags they are valid on
|
||||
* @type {Map<string, Map<string, Set<string>>>}
|
||||
*/
|
||||
const VALID_VALUES = new Map([
|
||||
['rel', rel],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Map between attributes and a mapping between pair-values and a set of values they are valid with
|
||||
* @type {Map<string, Map<string, Set<string>>>}
|
||||
*/
|
||||
const VALID_PAIR_VALUES = new Map([
|
||||
['rel', pairs],
|
||||
]);
|
||||
|
||||
/**
|
||||
* The set of all possible HTML elements. Used for skipping custom types
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const HTML_ELEMENTS = new Set([
|
||||
'a',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'address',
|
||||
'applet',
|
||||
'area',
|
||||
'article',
|
||||
'aside',
|
||||
'audio',
|
||||
'b',
|
||||
'base',
|
||||
'basefont',
|
||||
'bdi',
|
||||
'bdo',
|
||||
'bgsound',
|
||||
'big',
|
||||
'blink',
|
||||
'blockquote',
|
||||
'body',
|
||||
'br',
|
||||
'button',
|
||||
'canvas',
|
||||
'caption',
|
||||
'center',
|
||||
'cite',
|
||||
'code',
|
||||
'col',
|
||||
'colgroup',
|
||||
'content',
|
||||
'data',
|
||||
'datalist',
|
||||
'dd',
|
||||
'del',
|
||||
'details',
|
||||
'dfn',
|
||||
'dialog',
|
||||
'dir',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'embed',
|
||||
'fieldset',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'font',
|
||||
'footer',
|
||||
'form',
|
||||
'frame',
|
||||
'frameset',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'head',
|
||||
'header',
|
||||
'hgroup',
|
||||
'hr',
|
||||
'html',
|
||||
'i',
|
||||
'iframe',
|
||||
'image',
|
||||
'img',
|
||||
'input',
|
||||
'ins',
|
||||
'kbd',
|
||||
'keygen',
|
||||
'label',
|
||||
'legend',
|
||||
'li',
|
||||
'link',
|
||||
'main',
|
||||
'map',
|
||||
'mark',
|
||||
'marquee',
|
||||
'math',
|
||||
'menu',
|
||||
'menuitem',
|
||||
'meta',
|
||||
'meter',
|
||||
'nav',
|
||||
'nobr',
|
||||
'noembed',
|
||||
'noframes',
|
||||
'noscript',
|
||||
'object',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'option',
|
||||
'output',
|
||||
'p',
|
||||
'param',
|
||||
'picture',
|
||||
'plaintext',
|
||||
'portal',
|
||||
'pre',
|
||||
'progress',
|
||||
'q',
|
||||
'rb',
|
||||
'rp',
|
||||
'rt',
|
||||
'rtc',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'script',
|
||||
'section',
|
||||
'select',
|
||||
'shadow',
|
||||
'slot',
|
||||
'small',
|
||||
'source',
|
||||
'spacer',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'style',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'svg',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'template',
|
||||
'textarea',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'time',
|
||||
'title',
|
||||
'tr',
|
||||
'track',
|
||||
'tt',
|
||||
'u',
|
||||
'ul',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
'xmp',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Map between attributes and set of tags that the attribute is valid on
|
||||
* @type {Map<string, Set<string>>}
|
||||
*/
|
||||
const COMPONENT_ATTRIBUTE_MAP = new Map([
|
||||
['rel', new Set(['link', 'a', 'area', 'form'])],
|
||||
]);
|
||||
|
||||
/* eslint-disable eslint-plugin/no-unused-message-ids -- false positives, these messageIds are used */
|
||||
const messages = {
|
||||
emptyIsMeaningless: 'An empty “{{attributeName}}” attribute is meaningless.',
|
||||
neverValid: '“{{reportingValue}}” is never a valid “{{attributeName}}” attribute value.',
|
||||
noEmpty: 'An empty “{{attributeName}}” attribute is meaningless.',
|
||||
noMethod: 'The ”{{attributeName}}“ attribute cannot be a method.',
|
||||
notAlone: '“{{reportingValue}}” must be directly followed by “{{missingValue}}”.',
|
||||
notPaired: '“{{reportingValue}}” can not be directly followed by “{{secondValue}}” without “{{missingValue}}”.',
|
||||
notValidFor: '“{{reportingValue}}” is not a valid “{{attributeName}}” attribute value for <{{elementName}}>.',
|
||||
onlyMeaningfulFor: 'The ”{{attributeName}}“ attribute only has meaning on the tags: {{tagNames}}',
|
||||
onlyStrings: '“{{attributeName}}” attribute only supports strings.',
|
||||
spaceDelimited: '”{{attributeName}}“ attribute values should be space delimited.',
|
||||
suggestRemoveDefault: '"remove {{attributeName}}"',
|
||||
suggestRemoveEmpty: '"remove empty attribute {{attributeName}}"',
|
||||
suggestRemoveInvalid: '“remove invalid attribute {{reportingValue}}”',
|
||||
suggestRemoveWhitespaces: 'remove whitespaces in “{{attributeName}}”',
|
||||
suggestRemoveNonString: 'remove non-string value in “{{attributeName}}”',
|
||||
};
|
||||
|
||||
function splitIntoRangedParts(node, regex) {
|
||||
const valueRangeStart = node.range[0] + 1; // the plus one is for the initial quote
|
||||
|
||||
return Array.from(matchAll(node.value, regex), (match) => {
|
||||
const start = match.index + valueRangeStart;
|
||||
const end = start + match[0].length;
|
||||
|
||||
return {
|
||||
reportingValue: `${match[1]}`,
|
||||
value: match[1],
|
||||
range: [start, end],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function checkLiteralValueNode(context, attributeName, node, parentNode, parentNodeName) {
|
||||
if (typeof node.value !== 'string') {
|
||||
const data = { attributeName, reportingValue: node.value };
|
||||
|
||||
report(context, messages.onlyStrings, 'onlyStrings', {
|
||||
node,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveNonString',
|
||||
data,
|
||||
fix(fixer) { return fixer.remove(parentNode); },
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.value.trim()) {
|
||||
const data = { attributeName, reportingValue: node.value };
|
||||
|
||||
report(context, messages.noEmpty, 'noEmpty', {
|
||||
node,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveEmpty',
|
||||
data,
|
||||
fix(fixer) { return fixer.remove(node.parent); },
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const singleAttributeParts = splitIntoRangedParts(node, /(\S+)/g);
|
||||
singleAttributeParts.forEach((singlePart) => {
|
||||
const allowedTags = VALID_VALUES.get(attributeName).get(singlePart.value);
|
||||
const reportingValue = singlePart.reportingValue;
|
||||
|
||||
if (!allowedTags) {
|
||||
const data = {
|
||||
attributeName,
|
||||
reportingValue,
|
||||
};
|
||||
|
||||
const suggest = [{
|
||||
messageId: 'suggestRemoveInvalid',
|
||||
data,
|
||||
fix(fixer) { return fixer.removeRange(singlePart.range); },
|
||||
}];
|
||||
|
||||
report(context, messages.neverValid, 'neverValid', {
|
||||
node,
|
||||
data,
|
||||
suggest,
|
||||
});
|
||||
} else if (!allowedTags.has(parentNodeName)) {
|
||||
const data = {
|
||||
attributeName,
|
||||
reportingValue,
|
||||
elementName: parentNodeName,
|
||||
};
|
||||
|
||||
const suggest = [{
|
||||
messageId: 'suggestRemoveInvalid',
|
||||
data,
|
||||
fix(fixer) { return fixer.removeRange(singlePart.range); },
|
||||
}];
|
||||
|
||||
report(context, messages.notValidFor, 'notValidFor', {
|
||||
node,
|
||||
data,
|
||||
suggest,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const allowedPairsForAttribute = VALID_PAIR_VALUES.get(attributeName);
|
||||
if (allowedPairsForAttribute) {
|
||||
const pairAttributeParts = splitIntoRangedParts(node, /(?=(\b\S+\s*\S+))/g);
|
||||
pairAttributeParts.forEach((pairPart) => {
|
||||
allowedPairsForAttribute.forEach((siblings, pairing) => {
|
||||
const attributes = pairPart.reportingValue.split('\u0020');
|
||||
const firstValue = attributes[0];
|
||||
const secondValue = attributes[1];
|
||||
if (firstValue === pairing) {
|
||||
const lastValue = attributes[attributes.length - 1]; // in case of multiple white spaces
|
||||
if (!siblings.has(lastValue)) {
|
||||
const message = secondValue ? messages.notPaired : messages.notAlone;
|
||||
const messageId = secondValue ? 'notPaired' : 'notAlone';
|
||||
report(context, message, messageId, {
|
||||
node,
|
||||
data: {
|
||||
reportingValue: firstValue,
|
||||
secondValue,
|
||||
missingValue: Array.from(siblings).join(', '),
|
||||
},
|
||||
suggest: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const whitespaceParts = splitIntoRangedParts(node, /(\s+)/g);
|
||||
whitespaceParts.forEach((whitespacePart) => {
|
||||
const data = { attributeName };
|
||||
|
||||
if (whitespacePart.range[0] === (node.range[0] + 1) || whitespacePart.range[1] === (node.range[1] - 1)) {
|
||||
report(context, messages.spaceDelimited, 'spaceDelimited', {
|
||||
node,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveWhitespaces',
|
||||
data,
|
||||
fix(fixer) { return fixer.removeRange(whitespacePart.range); },
|
||||
}],
|
||||
});
|
||||
} else if (whitespacePart.value !== '\u0020') {
|
||||
report(context, messages.spaceDelimited, 'spaceDelimited', {
|
||||
node,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveWhitespaces',
|
||||
data,
|
||||
fix(fixer) { return fixer.replaceTextRange(whitespacePart.range, '\u0020'); },
|
||||
}],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const DEFAULT_ATTRIBUTES = ['rel'];
|
||||
|
||||
function checkAttribute(context, node) {
|
||||
const attribute = node.name.name;
|
||||
|
||||
const parentNodeName = node.parent.name.name;
|
||||
if (!COMPONENT_ATTRIBUTE_MAP.has(attribute) || !COMPONENT_ATTRIBUTE_MAP.get(attribute).has(parentNodeName)) {
|
||||
const tagNames = Array.from(
|
||||
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
|
||||
(tagName) => `"<${tagName}>"`
|
||||
).join(', ');
|
||||
const data = {
|
||||
attributeName: attribute,
|
||||
tagNames,
|
||||
};
|
||||
|
||||
report(context, messages.onlyMeaningfulFor, 'onlyMeaningfulFor', {
|
||||
node: node.name,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveDefault',
|
||||
data,
|
||||
fix(fixer) { return fixer.remove(node); },
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
function fix(fixer) { return fixer.remove(node); }
|
||||
|
||||
if (!node.value) {
|
||||
const data = { attributeName: attribute };
|
||||
|
||||
report(context, messages.emptyIsMeaningless, 'emptyIsMeaningless', {
|
||||
node: node.name,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveEmpty',
|
||||
data,
|
||||
fix,
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.value.type === 'Literal') {
|
||||
return checkLiteralValueNode(context, attribute, node.value, node, parentNodeName);
|
||||
}
|
||||
|
||||
if (node.value.expression.type === 'Literal') {
|
||||
return checkLiteralValueNode(context, attribute, node.value.expression, node, parentNodeName);
|
||||
}
|
||||
|
||||
if (node.value.type !== 'JSXExpressionContainer') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.value.expression.type === 'ObjectExpression') {
|
||||
const data = { attributeName: attribute };
|
||||
|
||||
report(context, messages.onlyStrings, 'onlyStrings', {
|
||||
node: node.value,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveDefault',
|
||||
data,
|
||||
fix,
|
||||
}],
|
||||
});
|
||||
} else if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
|
||||
const data = { attributeName: attribute };
|
||||
|
||||
report(context, messages.onlyStrings, 'onlyStrings', {
|
||||
node: node.value,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveDefault',
|
||||
data,
|
||||
fix,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isValidCreateElement(node) {
|
||||
return node.callee
|
||||
&& node.callee.type === 'MemberExpression'
|
||||
&& node.callee.object.name === 'React'
|
||||
&& node.callee.property.name === 'createElement'
|
||||
&& node.arguments.length > 0;
|
||||
}
|
||||
|
||||
function checkPropValidValue(context, node, value, attribute) {
|
||||
const validTags = VALID_VALUES.get(attribute);
|
||||
|
||||
if (value.type !== 'Literal') {
|
||||
return; // cannot check non-literals
|
||||
}
|
||||
|
||||
const validTagSet = validTags.get(value.value);
|
||||
if (!validTagSet) {
|
||||
const data = {
|
||||
attributeName: attribute,
|
||||
reportingValue: value.value,
|
||||
};
|
||||
|
||||
report(context, messages.neverValid, 'neverValid', {
|
||||
node: value,
|
||||
data,
|
||||
suggest: [{
|
||||
messageId: 'suggestRemoveInvalid',
|
||||
data,
|
||||
fix(fixer) { return fixer.replaceText(value, value.raw.replace(value.value, '')); },
|
||||
}],
|
||||
});
|
||||
} else if (!validTagSet.has(node.arguments[0].value)) {
|
||||
report(context, messages.notValidFor, 'notValidFor', {
|
||||
node: value,
|
||||
data: {
|
||||
attributeName: attribute,
|
||||
reportingValue: value.raw,
|
||||
elementName: node.arguments[0].value,
|
||||
},
|
||||
suggest: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} context
|
||||
* @param {*} node
|
||||
* @param {string} attribute
|
||||
*/
|
||||
function checkCreateProps(context, node, attribute) {
|
||||
const propsArg = node.arguments[1];
|
||||
|
||||
if (!propsArg || propsArg.type !== 'ObjectExpression') {
|
||||
return; // can't check variables, computed, or shorthands
|
||||
}
|
||||
|
||||
for (const prop of propsArg.properties) {
|
||||
if (!prop.key || prop.key.type !== 'Identifier') {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue; // cannot check computed keys
|
||||
}
|
||||
|
||||
if (prop.key.name !== attribute) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue; // ignore not this attribute
|
||||
}
|
||||
|
||||
if (!COMPONENT_ATTRIBUTE_MAP.get(attribute).has(node.arguments[0].value)) {
|
||||
const tagNames = Array.from(
|
||||
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
|
||||
(tagName) => `"<${tagName}>"`
|
||||
).join(', ');
|
||||
|
||||
report(context, messages.onlyMeaningfulFor, 'onlyMeaningfulFor', {
|
||||
node: prop.key,
|
||||
data: {
|
||||
attributeName: attribute,
|
||||
tagNames,
|
||||
},
|
||||
suggest: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop.method) {
|
||||
report(context, messages.noMethod, 'noMethod', {
|
||||
node: prop,
|
||||
data: {
|
||||
attributeName: attribute,
|
||||
},
|
||||
suggest: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop.shorthand || prop.computed) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue; // cannot check these
|
||||
}
|
||||
|
||||
if (prop.value.type === 'ArrayExpression') {
|
||||
prop.value.elements.forEach((value) => {
|
||||
checkPropValidValue(context, node, value, attribute);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
checkPropValidValue(context, node, prop.value, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of invalid attributes',
|
||||
category: 'Possible Errors',
|
||||
url: docsUrl('no-invalid-html-attribute'),
|
||||
},
|
||||
messages,
|
||||
schema: [{
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
enum: ['rel'],
|
||||
},
|
||||
}],
|
||||
type: 'suggestion',
|
||||
hasSuggestions: true, // eslint-disable-line eslint-plugin/require-meta-has-suggestions
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
|
||||
|
||||
// ignore attributes that aren't configured to be checked
|
||||
if (!attributes.has(node.name.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore non-HTML elements
|
||||
if (!HTML_ELEMENTS.has(node.parent.name.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkAttribute(context, node);
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!isValidCreateElement(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elemNameArg = node.arguments[0];
|
||||
|
||||
if (!elemNameArg || elemNameArg.type !== 'Literal') {
|
||||
return; // can only check literals
|
||||
}
|
||||
|
||||
// ignore non-HTML elements
|
||||
if (typeof elemNameArg.value === 'string' && !HTML_ELEMENTS.has(elemNameArg.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
|
||||
|
||||
attributes.forEach((attribute) => {
|
||||
checkCreateProps(context, node, attribute);
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
61
node_modules/eslint-plugin-react/lib/rules/no-is-mounted.js
generated
vendored
Normal file
61
node_modules/eslint-plugin-react/lib/rules/no-is-mounted.js
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of isMounted
|
||||
* @author Joe Lencioni
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getAncestors = require('../util/eslint').getAncestors;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noIsMounted: 'Do not use isMounted',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of isMounted',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-is-mounted'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (callee.type !== 'MemberExpression') {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
callee.object.type !== 'ThisExpression'
|
||||
|| !('name' in callee.property)
|
||||
|| callee.property.name !== 'isMounted'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const ancestors = getAncestors(context, node);
|
||||
for (let i = 0, j = ancestors.length; i < j; i++) {
|
||||
if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') {
|
||||
report(context, messages.noIsMounted, 'noIsMounted', {
|
||||
node: callee,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
81
node_modules/eslint-plugin-react/lib/rules/no-multi-comp.js
generated
vendored
Normal file
81
node_modules/eslint-plugin-react/lib/rules/no-multi-comp.js
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @fileoverview Prevent multiple component definition per file
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
onlyOneComponent: 'Declare only one React component per file',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow multiple component definition per file',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('no-multi-comp'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreStateless: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreStateless = configuration.ignoreStateless || false;
|
||||
|
||||
/**
|
||||
* Checks if the component is ignored
|
||||
* @param {Object} component The component being checked.
|
||||
* @returns {boolean} True if the component is ignored, false if not.
|
||||
*/
|
||||
function isIgnored(component) {
|
||||
return (
|
||||
ignoreStateless && (
|
||||
/Function/.test(component.node.type)
|
||||
|| utils.isPragmaComponentWrapper(component.node)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
if (components.length() <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
values(components.list())
|
||||
.filter((component) => !isIgnored(component))
|
||||
.slice(1)
|
||||
.forEach((component) => {
|
||||
report(context, messages.onlyOneComponent, 'onlyOneComponent', {
|
||||
node: component.node,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
62
node_modules/eslint-plugin-react/lib/rules/no-namespace.js
generated
vendored
Normal file
62
node_modules/eslint-plugin-react/lib/rules/no-namespace.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @fileoverview Enforce that namespaces are not used in React elements
|
||||
* @author Yacine Hmito
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const elementType = require('jsx-ast-utils/elementType');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noNamespace: 'React component {{name}} must not be in a namespace, as React does not support them',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce that namespaces are not used in React elements',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-namespace'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (isCreateElement(context, node) && node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
|
||||
const name = node.arguments[0].value;
|
||||
if (typeof name !== 'string' || name.indexOf(':') === -1) return undefined;
|
||||
report(context, messages.noNamespace, 'noNamespace', {
|
||||
node,
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
JSXOpeningElement(node) {
|
||||
const name = elementType(node);
|
||||
if (typeof name !== 'string' || name.indexOf(':') === -1) return undefined;
|
||||
report(context, messages.noNamespace, 'noNamespace', {
|
||||
node,
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
105
node_modules/eslint-plugin-react/lib/rules/no-object-type-as-default-prop.js
generated
vendored
Normal file
105
node_modules/eslint-plugin-react/lib/rules/no-object-type-as-default-prop.js
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of referential-type variables as default param in functional component
|
||||
* @author Chang Yan
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const report = require('../util/report');
|
||||
|
||||
const FORBIDDEN_TYPES_MAP = {
|
||||
ArrowFunctionExpression: 'arrow function',
|
||||
FunctionExpression: 'function expression',
|
||||
ObjectExpression: 'object literal',
|
||||
ArrayExpression: 'array literal',
|
||||
ClassExpression: 'class expression',
|
||||
NewExpression: 'construction expression',
|
||||
JSXElement: 'JSX element',
|
||||
};
|
||||
|
||||
const FORBIDDEN_TYPES = new Set(Object.keys(FORBIDDEN_TYPES_MAP));
|
||||
const MESSAGE_ID = 'forbiddenTypeDefaultParam';
|
||||
|
||||
const messages = {
|
||||
[MESSAGE_ID]: '{{propName}} has a/an {{forbiddenType}} as default prop. This could lead to potential infinite render loop in React. Use a variable reference instead of {{forbiddenType}}.',
|
||||
};
|
||||
function hasUsedObjectDestructuringSyntax(params) {
|
||||
return (
|
||||
params != null
|
||||
&& params.length >= 1
|
||||
&& params[0].type === 'ObjectPattern'
|
||||
);
|
||||
}
|
||||
|
||||
function verifyDefaultPropsDestructuring(context, properties) {
|
||||
// Loop through each of the default params
|
||||
properties.filter((prop) => prop.type === 'Property' && prop.value.type === 'AssignmentPattern').forEach((prop) => {
|
||||
const propName = prop.key.name;
|
||||
const propDefaultValue = prop.value;
|
||||
|
||||
const propDefaultValueType = propDefaultValue.right.type;
|
||||
|
||||
if (
|
||||
propDefaultValueType === 'Literal'
|
||||
&& propDefaultValue.right.regex != null
|
||||
) {
|
||||
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
|
||||
node: propDefaultValue,
|
||||
data: {
|
||||
propName,
|
||||
forbiddenType: 'regex literal',
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
astUtil.isCallExpression(propDefaultValue.right)
|
||||
&& propDefaultValue.right.callee.type === 'Identifier'
|
||||
&& propDefaultValue.right.callee.name === 'Symbol'
|
||||
) {
|
||||
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
|
||||
node: propDefaultValue,
|
||||
data: {
|
||||
propName,
|
||||
forbiddenType: 'Symbol literal',
|
||||
},
|
||||
});
|
||||
} else if (FORBIDDEN_TYPES.has(propDefaultValueType)) {
|
||||
report(context, messages[MESSAGE_ID], MESSAGE_ID, {
|
||||
node: propDefaultValue,
|
||||
data: {
|
||||
propName,
|
||||
forbiddenType: FORBIDDEN_TYPES_MAP[propDefaultValueType],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of referential-type variables as default param in functional component',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-object-type-as-default-prop'),
|
||||
},
|
||||
messages,
|
||||
},
|
||||
create: Components.detect((context, components) => ({
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
values(list)
|
||||
.filter((component) => hasUsedObjectDestructuringSyntax(component.node.params))
|
||||
.forEach((component) => {
|
||||
const node = component.node;
|
||||
const properties = node.params[0].properties;
|
||||
verifyDefaultPropsDestructuring(context, properties);
|
||||
});
|
||||
},
|
||||
})),
|
||||
};
|
||||
88
node_modules/eslint-plugin-react/lib/rules/no-redundant-should-component-update.js
generated
vendored
Normal file
88
node_modules/eslint-plugin-react/lib/rules/no-redundant-should-component-update.js
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @fileoverview Flag shouldComponentUpdate when extending PureComponent
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noShouldCompUpdate: '{{component}} does not need shouldComponentUpdate when extending React.PureComponent.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of shouldComponentUpdate when extending React.PureComponent',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-redundant-should-component-update'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
/**
|
||||
* Checks for shouldComponentUpdate property
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} Whether or not the property exists.
|
||||
*/
|
||||
function hasShouldComponentUpdate(node) {
|
||||
const properties = astUtil.getComponentProperties(node);
|
||||
return properties.some((property) => {
|
||||
const name = astUtil.getPropertyName(property);
|
||||
return name === 'shouldComponentUpdate';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of node if available
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @return {string} The name of the node
|
||||
*/
|
||||
function getNodeName(node) {
|
||||
if (node.id) {
|
||||
return node.id.name;
|
||||
}
|
||||
if (node.parent && node.parent.id) {
|
||||
return node.parent.id.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for violation of rule
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function checkForViolation(node) {
|
||||
if (componentUtil.isPureComponent(node, context)) {
|
||||
const hasScu = hasShouldComponentUpdate(node);
|
||||
if (hasScu) {
|
||||
const className = getNodeName(node);
|
||||
report(context, messages.noShouldCompUpdate, 'noShouldCompUpdate', {
|
||||
node,
|
||||
data: {
|
||||
component: className,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: checkForViolation,
|
||||
ClassExpression: checkForViolation,
|
||||
};
|
||||
},
|
||||
};
|
||||
82
node_modules/eslint-plugin-react/lib/rules/no-render-return-value.js
generated
vendored
Normal file
82
node_modules/eslint-plugin-react/lib/rules/no-render-return-value.js
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of the return value of React.render
|
||||
* @author Dustan Kasten
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noReturnValue: 'Do not depend on the return value from {{node}}.render',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of the return value of ReactDOM.render',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-render-return-value'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
let calleeObjectName = /^ReactDOM$/;
|
||||
if (testReactVersion(context, '>= 15.0.0')) {
|
||||
calleeObjectName = /^ReactDOM$/;
|
||||
} else if (testReactVersion(context, '^0.14.0')) {
|
||||
calleeObjectName = /^React(DOM)?$/;
|
||||
} else if (testReactVersion(context, '^0.13.0')) {
|
||||
calleeObjectName = /^React$/;
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
const parent = node.parent;
|
||||
if (callee.type !== 'MemberExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
callee.object.type !== 'Identifier'
|
||||
|| !calleeObjectName.test(callee.object.name)
|
||||
|| (!('name' in callee.property) || callee.property.name !== 'render')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
parent.type === 'VariableDeclarator'
|
||||
|| parent.type === 'Property'
|
||||
|| parent.type === 'ReturnStatement'
|
||||
|| parent.type === 'ArrowFunctionExpression'
|
||||
|| parent.type === 'AssignmentExpression'
|
||||
) {
|
||||
report(context, messages.noReturnValue, 'noReturnValue', {
|
||||
node: callee,
|
||||
data: {
|
||||
node: callee.object.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
88
node_modules/eslint-plugin-react/lib/rules/no-set-state.js
generated
vendored
Normal file
88
node_modules/eslint-plugin-react/lib/rules/no-set-state.js
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState
|
||||
* @author Mark Dalgleish
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noSetState: 'Do not use setState',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of setState',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('no-set-state'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
/**
|
||||
* Checks if the component is valid
|
||||
* @param {Object} component The component to process
|
||||
* @returns {boolean} True if the component is valid, false if not.
|
||||
*/
|
||||
function isValid(component) {
|
||||
return !!component && !component.useSetState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports usages of setState for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportSetStateUsages(component) {
|
||||
for (let i = 0, j = component.setStateUsages.length; i < j; i++) {
|
||||
const setStateUsage = component.setStateUsages[i];
|
||||
report(context, messages.noSetState, 'noSetState', {
|
||||
node: setStateUsage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (
|
||||
callee.type !== 'MemberExpression'
|
||||
|| callee.object.type !== 'ThisExpression'
|
||||
|| callee.property.name !== 'setState'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const component = components.get(utils.getParentComponent(node));
|
||||
const setStateUsages = (component && component.setStateUsages) || [];
|
||||
setStateUsages.push(callee);
|
||||
components.set(node, {
|
||||
useSetState: true,
|
||||
setStateUsages,
|
||||
});
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
values(components.list())
|
||||
.filter((component) => !isValid(component))
|
||||
.forEach((component) => {
|
||||
reportSetStateUsages(component);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
117
node_modules/eslint-plugin-react/lib/rules/no-string-refs.js
generated
vendored
Normal file
117
node_modules/eslint-plugin-react/lib/rules/no-string-refs.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Prevent string definitions for references and prevent referencing this.refs
|
||||
* @author Tom Hastjarjanto
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
thisRefsDeprecated: 'Using this.refs is deprecated.',
|
||||
stringInRefDeprecated: 'Using string literals in ref attributes is deprecated.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow using string references',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('no-string-refs'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
noTemplateLiterals: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const checkRefsUsage = testReactVersion(context, '< 18.3.0'); // `this.refs` is writable in React 18.3.0 and later, see https://github.com/facebook/react/pull/28867
|
||||
const detectTemplateLiterals = context.options[0] ? context.options[0].noTemplateLiterals : false;
|
||||
/**
|
||||
* Checks if we are using refs
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if we are using refs, false if not.
|
||||
*/
|
||||
function isRefsUsage(node) {
|
||||
return !!(
|
||||
(componentUtil.getParentES6Component(context, node) || componentUtil.getParentES5Component(context, node))
|
||||
&& node.object.type === 'ThisExpression'
|
||||
&& node.property.name === 'refs'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are using a ref attribute
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if we are using a ref attribute, false if not.
|
||||
*/
|
||||
function isRefAttribute(node) {
|
||||
return node.type === 'JSXAttribute'
|
||||
&& !!node.name
|
||||
&& node.name.name === 'ref';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node contains a string value
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if the node contains a string value, false if not.
|
||||
*/
|
||||
function containsStringLiteral(node) {
|
||||
return !!node.value
|
||||
&& node.value.type === 'Literal'
|
||||
&& typeof node.value.value === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node contains a string value within a jsx expression
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if the node contains a string value within a jsx expression, false if not.
|
||||
*/
|
||||
function containsStringExpressionContainer(node) {
|
||||
return !!node.value
|
||||
&& node.value.type === 'JSXExpressionContainer'
|
||||
&& node.value.expression
|
||||
&& ((node.value.expression.type === 'Literal' && typeof node.value.expression.value === 'string')
|
||||
|| (node.value.expression.type === 'TemplateLiteral' && detectTemplateLiterals));
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (checkRefsUsage && isRefsUsage(node)) {
|
||||
report(context, messages.thisRefsDeprecated, 'thisRefsDeprecated', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(node) {
|
||||
if (
|
||||
isRefAttribute(node)
|
||||
&& (containsStringLiteral(node) || containsStringExpressionContainer(node))
|
||||
) {
|
||||
report(context, messages.stringInRefDeprecated, 'stringInRefDeprecated', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
47
node_modules/eslint-plugin-react/lib/rules/no-this-in-sfc.js
generated
vendored
Normal file
47
node_modules/eslint-plugin-react/lib/rules/no-this-in-sfc.js
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Report "this" being used in stateless functional components.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noThisInSFC: 'Stateless functional components should not use `this`',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow `this` from being used in stateless functional components',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-this-in-sfc'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => ({
|
||||
MemberExpression(node) {
|
||||
if (node.object.type === 'ThisExpression') {
|
||||
const component = components.get(utils.getParentStatelessComponent(node));
|
||||
if (!component || (component.node && component.node.parent && component.node.parent.type === 'Property')) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.noThisInSFC, 'noThisInSFC', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
})),
|
||||
};
|
||||
260
node_modules/eslint-plugin-react/lib/rules/no-typos.js
generated
vendored
Normal file
260
node_modules/eslint-plugin-react/lib/rules/no-typos.js
generated
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* @fileoverview Prevent common casing typos
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const PROP_TYPES = Object.keys(require('prop-types'));
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const report = require('../util/report');
|
||||
const lifecycleMethods = require('../util/lifecycleMethods');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const STATIC_CLASS_PROPERTIES = ['propTypes', 'contextTypes', 'childContextTypes', 'defaultProps'];
|
||||
|
||||
const messages = {
|
||||
typoPropTypeChain: 'Typo in prop type chain qualifier: {{name}}',
|
||||
typoPropType: 'Typo in declared prop type: {{name}}',
|
||||
typoStaticClassProp: 'Typo in static class property declaration',
|
||||
typoPropDeclaration: 'Typo in property declaration',
|
||||
typoLifecycleMethod: 'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}',
|
||||
staticLifecycleMethod: 'Lifecycle method should be static: {{method}}',
|
||||
noPropTypesBinding: '`\'prop-types\'` imported without a local `PropTypes` binding.',
|
||||
noReactBinding: '`\'react\'` imported without a local `React` binding.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow common typos',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('no-typos'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
let propTypesPackageName = null;
|
||||
let reactPackageName = null;
|
||||
|
||||
function checkValidPropTypeQualifier(node) {
|
||||
if (node.name !== 'isRequired') {
|
||||
report(context, messages.typoPropTypeChain, 'typoPropTypeChain', {
|
||||
node,
|
||||
data: { name: node.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkValidPropType(node) {
|
||||
if (node.name && !PROP_TYPES.some((propTypeName) => propTypeName === node.name)) {
|
||||
report(context, messages.typoPropType, 'typoPropType', {
|
||||
node,
|
||||
data: { name: node.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isPropTypesPackage(node) {
|
||||
return (
|
||||
node.type === 'Identifier'
|
||||
&& node.name === propTypesPackageName
|
||||
) || (
|
||||
node.type === 'MemberExpression'
|
||||
&& node.property.name === 'PropTypes'
|
||||
&& node.object.name === reactPackageName
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
function checkValidCallExpression(node) {
|
||||
const callee = node.callee;
|
||||
if (callee.type === 'MemberExpression' && callee.property.name === 'shape') {
|
||||
checkValidPropObject(node.arguments[0]);
|
||||
} else if (callee.type === 'MemberExpression' && callee.property.name === 'oneOfType') {
|
||||
const args = node.arguments[0];
|
||||
if (args && args.type === 'ArrayExpression') {
|
||||
args.elements.forEach((el) => {
|
||||
checkValidProp(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkValidProp(node) {
|
||||
if ((!propTypesPackageName && !reactPackageName) || !node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'MemberExpression') {
|
||||
if (
|
||||
node.object.type === 'MemberExpression'
|
||||
&& isPropTypesPackage(node.object.object)
|
||||
) { // PropTypes.myProp.isRequired
|
||||
checkValidPropType(node.object.property);
|
||||
checkValidPropTypeQualifier(node.property);
|
||||
} else if (
|
||||
isPropTypesPackage(node.object)
|
||||
&& node.property.name !== 'isRequired'
|
||||
) { // PropTypes.myProp
|
||||
checkValidPropType(node.property);
|
||||
} else if (astUtil.isCallExpression(node.object)) {
|
||||
checkValidPropTypeQualifier(node.property);
|
||||
checkValidCallExpression(node.object);
|
||||
}
|
||||
} else if (astUtil.isCallExpression(node)) {
|
||||
checkValidCallExpression(node);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
function checkValidPropObject(node) {
|
||||
if (node && node.type === 'ObjectExpression') {
|
||||
node.properties.forEach((prop) => checkValidProp(prop.value));
|
||||
}
|
||||
}
|
||||
|
||||
function reportErrorIfPropertyCasingTypo(propertyValue, propertyKey, isClassProperty) {
|
||||
const propertyName = propertyKey.name;
|
||||
if (propertyName === 'propTypes' || propertyName === 'contextTypes' || propertyName === 'childContextTypes') {
|
||||
checkValidPropObject(propertyValue);
|
||||
}
|
||||
STATIC_CLASS_PROPERTIES.forEach((CLASS_PROP) => {
|
||||
if (propertyName && CLASS_PROP.toLowerCase() === propertyName.toLowerCase() && CLASS_PROP !== propertyName) {
|
||||
const messageId = isClassProperty
|
||||
? 'typoStaticClassProp'
|
||||
: 'typoPropDeclaration';
|
||||
report(context, messages[messageId], messageId, {
|
||||
node: propertyKey,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reportErrorIfLifecycleMethodCasingTypo(node) {
|
||||
const key = node.key;
|
||||
let nodeKeyName = key.name;
|
||||
if (key.type === 'Literal') {
|
||||
nodeKeyName = key.value;
|
||||
}
|
||||
if (key.type === 'PrivateName' || (node.computed && typeof nodeKeyName !== 'string')) {
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycleMethods.static.forEach((method) => {
|
||||
if (!node.static && nodeKeyName && nodeKeyName.toLowerCase() === method.toLowerCase()) {
|
||||
report(context, messages.staticLifecycleMethod, 'staticLifecycleMethod', {
|
||||
node,
|
||||
data: {
|
||||
method: nodeKeyName,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleMethods.instance.concat(lifecycleMethods.static).forEach((method) => {
|
||||
if (nodeKeyName && method.toLowerCase() === nodeKeyName.toLowerCase() && method !== nodeKeyName) {
|
||||
report(context, messages.typoLifecycleMethod, 'typoLifecycleMethod', {
|
||||
node,
|
||||
data: { actual: nodeKeyName, expected: method },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.source && node.source.value === 'prop-types') { // import PropType from "prop-types"
|
||||
if (node.specifiers.length > 0) {
|
||||
propTypesPackageName = node.specifiers[0].local.name;
|
||||
} else {
|
||||
report(context, messages.noPropTypesBinding, 'noPropTypesBinding', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
} else if (node.source && node.source.value === 'react') { // import { PropTypes } from "react"
|
||||
if (node.specifiers.length > 0) {
|
||||
reactPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "react"`
|
||||
} else {
|
||||
report(context, messages.noReactBinding, 'noReactBinding', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
if (node.specifiers.length >= 1) {
|
||||
const propTypesSpecifier = node.specifiers.find((specifier) => (
|
||||
specifier.imported
|
||||
&& specifier.imported.name === 'PropTypes'
|
||||
));
|
||||
if (propTypesSpecifier) {
|
||||
propTypesPackageName = propTypesSpecifier.local.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportErrorIfPropertyCasingTypo(node.value, node.key, true);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
const propertyName = node.property.name;
|
||||
|
||||
if (
|
||||
!propertyName
|
||||
|| STATIC_CLASS_PROPERTIES.map((prop) => prop.toLocaleLowerCase()).indexOf(propertyName.toLowerCase()) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedComponent = utils.getRelatedComponent(node);
|
||||
|
||||
if (
|
||||
relatedComponent
|
||||
&& (componentUtil.isES6Component(relatedComponent.node, context) || (
|
||||
relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node)))
|
||||
&& (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
|
||||
) {
|
||||
reportErrorIfPropertyCasingTypo(node.parent.right, node.property, true);
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (!componentUtil.isES6Component(node.parent.parent, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportErrorIfLifecycleMethodCasingTypo(node);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
const component = componentUtil.isES5Component(node, context) && components.get(node);
|
||||
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.properties.filter((property) => property.type !== 'SpreadElement').forEach((property) => {
|
||||
reportErrorIfPropertyCasingTypo(property.value, property.key, false);
|
||||
reportErrorIfLifecycleMethodCasingTypo(property);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
135
node_modules/eslint-plugin-react/lib/rules/no-unescaped-entities.js
generated
vendored
Normal file
135
node_modules/eslint-plugin-react/lib/rules/no-unescaped-entities.js
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @fileoverview HTML special characters should be escaped.
|
||||
* @author Patrick Hayes
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getSourceCode = require('../util/eslint').getSourceCode;
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
// NOTE: '<' and '{' are also problematic characters, but they do not need
|
||||
// to be included here because it is a syntax error when these characters are
|
||||
// included accidentally.
|
||||
const DEFAULTS = [{
|
||||
char: '>',
|
||||
alternatives: ['>'],
|
||||
}, {
|
||||
char: '"',
|
||||
alternatives: ['"', '“', '"', '”'],
|
||||
}, {
|
||||
char: '\'',
|
||||
alternatives: [''', '‘', ''', '’'],
|
||||
}, {
|
||||
char: '}',
|
||||
alternatives: ['}'],
|
||||
}];
|
||||
|
||||
const messages = {
|
||||
unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
|
||||
unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow unescaped HTML entities from appearing in markup',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('no-unescaped-entities'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbid: {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: [{
|
||||
type: 'string',
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
char: {
|
||||
type: 'string',
|
||||
},
|
||||
alternatives: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function reportInvalidEntity(node) {
|
||||
const configuration = context.options[0] || {};
|
||||
const entities = configuration.forbid || DEFAULTS;
|
||||
|
||||
// HTML entities are already escaped in node.value (as well as node.raw),
|
||||
// so pull the raw text from getSourceCode(context)
|
||||
for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
|
||||
let rawLine = getSourceCode(context).lines[i - 1];
|
||||
let start = 0;
|
||||
let end = rawLine.length;
|
||||
if (i === node.loc.start.line) {
|
||||
start = node.loc.start.column;
|
||||
}
|
||||
if (i === node.loc.end.line) {
|
||||
end = node.loc.end.column;
|
||||
}
|
||||
rawLine = rawLine.slice(start, end);
|
||||
for (let j = 0; j < entities.length; j++) {
|
||||
for (let index = 0; index < rawLine.length; index++) {
|
||||
const c = rawLine[index];
|
||||
if (typeof entities[j] === 'string') {
|
||||
if (c === entities[j]) {
|
||||
report(context, messages.unescapedEntity, 'unescapedEntity', {
|
||||
node,
|
||||
loc: { line: i, column: start + index },
|
||||
data: {
|
||||
entity: entities[j],
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (c === entities[j].char) {
|
||||
report(context, messages.unescapedEntityAlts, 'unescapedEntityAlts', {
|
||||
node,
|
||||
loc: { line: i, column: start + index },
|
||||
data: {
|
||||
entity: entities[j].char,
|
||||
alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'Literal, JSXText'(node) {
|
||||
if (jsxUtil.isJSX(node.parent)) {
|
||||
reportInvalidEntity(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
655
node_modules/eslint-plugin-react/lib/rules/no-unknown-property.js
generated
vendored
Normal file
655
node_modules/eslint-plugin-react/lib/rules/no-unknown-property.js
generated
vendored
Normal file
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of unknown DOM property
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const getText = require('../util/eslint').getText;
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULTS = {
|
||||
ignore: [],
|
||||
requireDataLowercase: false,
|
||||
};
|
||||
|
||||
const DOM_ATTRIBUTE_NAMES = {
|
||||
'accept-charset': 'acceptCharset',
|
||||
class: 'className',
|
||||
'http-equiv': 'httpEquiv',
|
||||
crossorigin: 'crossOrigin',
|
||||
for: 'htmlFor',
|
||||
nomodule: 'noModule',
|
||||
};
|
||||
|
||||
const ATTRIBUTE_TAGS_MAP = {
|
||||
abbr: ['th', 'td'],
|
||||
charset: ['meta'],
|
||||
checked: ['input'],
|
||||
// image is required for SVG support, all other tags are HTML.
|
||||
crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'],
|
||||
displaystyle: ['math'],
|
||||
// https://html.spec.whatwg.org/multipage/links.html#downloading-resources
|
||||
download: ['a', 'area'],
|
||||
fill: [ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
|
||||
// Fill color
|
||||
'altGlyph',
|
||||
'circle',
|
||||
'ellipse',
|
||||
'g',
|
||||
'line',
|
||||
'marker',
|
||||
'mask',
|
||||
'path',
|
||||
'polygon',
|
||||
'polyline',
|
||||
'rect',
|
||||
'svg',
|
||||
'symbol',
|
||||
'text',
|
||||
'textPath',
|
||||
'tref',
|
||||
'tspan',
|
||||
'use',
|
||||
// Animation final state
|
||||
'animate',
|
||||
'animateColor',
|
||||
'animateMotion',
|
||||
'animateTransform',
|
||||
'set',
|
||||
],
|
||||
focusable: ['svg'],
|
||||
imageSizes: ['link'],
|
||||
imageSrcSet: ['link'],
|
||||
property: ['meta'],
|
||||
viewBox: ['marker', 'pattern', 'svg', 'symbol', 'view'],
|
||||
as: ['link'],
|
||||
align: ['applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'], // deprecated, but known
|
||||
valign: ['tr', 'td', 'th', 'thead', 'tbody', 'tfoot', 'colgroup', 'col'], // deprecated, but known
|
||||
noModule: ['script'],
|
||||
// Media events allowed only on audio and video tags, see https://github.com/facebook/react/blob/256aefbea1449869620fb26f6ec695536ab453f5/CHANGELOG.md#notable-enhancements
|
||||
onAbort: ['audio', 'video'],
|
||||
onCancel: ['dialog'],
|
||||
onCanPlay: ['audio', 'video'],
|
||||
onCanPlayThrough: ['audio', 'video'],
|
||||
onClose: ['dialog'],
|
||||
onDurationChange: ['audio', 'video'],
|
||||
onEmptied: ['audio', 'video'],
|
||||
onEncrypted: ['audio', 'video'],
|
||||
onEnded: ['audio', 'video'],
|
||||
onError: ['audio', 'video', 'img', 'link', 'source', 'script', 'picture', 'iframe'],
|
||||
onLoad: ['script', 'img', 'link', 'picture', 'iframe', 'object', 'source'],
|
||||
onLoadedData: ['audio', 'video'],
|
||||
onLoadedMetadata: ['audio', 'video'],
|
||||
onLoadStart: ['audio', 'video'],
|
||||
onPause: ['audio', 'video'],
|
||||
onPlay: ['audio', 'video'],
|
||||
onPlaying: ['audio', 'video'],
|
||||
onProgress: ['audio', 'video'],
|
||||
onRateChange: ['audio', 'video'],
|
||||
onResize: ['audio', 'video'],
|
||||
onSeeked: ['audio', 'video'],
|
||||
onSeeking: ['audio', 'video'],
|
||||
onStalled: ['audio', 'video'],
|
||||
onSuspend: ['audio', 'video'],
|
||||
onTimeUpdate: ['audio', 'video'],
|
||||
onVolumeChange: ['audio', 'video'],
|
||||
onWaiting: ['audio', 'video'],
|
||||
autoPictureInPicture: ['video'],
|
||||
controls: ['audio', 'video'],
|
||||
controlsList: ['audio', 'video'],
|
||||
disablePictureInPicture: ['video'],
|
||||
disableRemotePlayback: ['audio', 'video'],
|
||||
loop: ['audio', 'video'],
|
||||
muted: ['audio', 'video'],
|
||||
playsInline: ['video'],
|
||||
allowFullScreen: ['iframe', 'video'],
|
||||
webkitAllowFullScreen: ['iframe', 'video'],
|
||||
mozAllowFullScreen: ['iframe', 'video'],
|
||||
poster: ['video'],
|
||||
preload: ['audio', 'video'],
|
||||
scrolling: ['iframe'],
|
||||
returnValue: ['dialog'],
|
||||
webkitDirectory: ['input'],
|
||||
};
|
||||
|
||||
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_ONE_WORD = [
|
||||
// Global attributes - can be used on any HTML/DOM element
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
|
||||
'dir', 'draggable', 'hidden', 'id', 'lang', 'nonce', 'part', 'slot', 'style', 'title', 'translate', 'inert',
|
||||
// Element specific attributes
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
|
||||
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
|
||||
'accept', 'action', 'allow', 'alt', 'as', 'async', 'buffered', 'capture', 'challenge', 'cite', 'code', 'cols',
|
||||
'content', 'coords', 'csp', 'data', 'decoding', 'default', 'defer', 'disabled', 'form',
|
||||
'headers', 'height', 'high', 'href', 'icon', 'importance', 'integrity', 'kind', 'label',
|
||||
'language', 'loading', 'list', 'loop', 'low', 'manifest', 'max', 'media', 'method', 'min', 'multiple', 'muted',
|
||||
'name', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'profile',
|
||||
'rel', 'required', 'reversed', 'role', 'rows', 'sandbox', 'scope', 'seamless', 'selected', 'shape', 'size', 'sizes',
|
||||
'span', 'src', 'start', 'step', 'summary', 'target', 'type', 'value', 'width', 'wmode', 'wrap',
|
||||
// SVG attributes
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
||||
'accumulate', 'additive', 'alphabetic', 'amplitude', 'ascent', 'azimuth', 'bbox', 'begin',
|
||||
'bias', 'by', 'clip', 'color', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'direction',
|
||||
'display', 'divisor', 'dur', 'dx', 'dy', 'elevation', 'end', 'exponent', 'fill', 'filter',
|
||||
'format', 'from', 'fr', 'fx', 'fy', 'g1', 'g2', 'hanging', 'height', 'hreflang', 'ideographic',
|
||||
'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'local', 'mask', 'mode',
|
||||
'offset', 'opacity', 'operator', 'order', 'orient', 'orientation', 'origin', 'overflow', 'path',
|
||||
'ping', 'points', 'r', 'radius', 'rel', 'restart', 'result', 'rotate', 'rx', 'ry', 'scale',
|
||||
'seed', 'slope', 'spacing', 'speed', 'stemh', 'stemv', 'string', 'stroke', 'to', 'transform',
|
||||
'u1', 'u2', 'unicode', 'values', 'version', 'visibility', 'widths', 'x', 'x1', 'x2', 'xmlns',
|
||||
'y', 'y1', 'y2', 'z',
|
||||
// OpenGraph meta tag attributes
|
||||
'property',
|
||||
// React specific attributes
|
||||
'ref', 'key', 'children',
|
||||
// Non-standard
|
||||
'results', 'security',
|
||||
// Video specific
|
||||
'controls',
|
||||
// popovers
|
||||
'popover', 'popovertarget', 'popovertargetaction',
|
||||
];
|
||||
|
||||
const DOM_PROPERTY_NAMES_TWO_WORDS = [
|
||||
// Global attributes - can be used on any HTML/DOM element
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
|
||||
'accessKey', 'autoCapitalize', 'autoFocus', 'contentEditable', 'enterKeyHint', 'exportParts',
|
||||
'inputMode', 'itemID', 'itemRef', 'itemProp', 'itemScope', 'itemType', 'spellCheck', 'tabIndex',
|
||||
// Element specific attributes
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes (includes global attributes too)
|
||||
// To be considered if these should be added also to ATTRIBUTE_TAGS_MAP
|
||||
'acceptCharset', 'autoComplete', 'autoPlay', 'border', 'cellPadding', 'cellSpacing', 'classID', 'codeBase',
|
||||
'colSpan', 'contextMenu', 'dateTime', 'encType', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 'formTarget',
|
||||
'frameBorder', 'hrefLang', 'httpEquiv', 'imageSizes', 'imageSrcSet', 'isMap', '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', 'onToggle', 'onTransitionEnd', 'radioGroup', 'readOnly', 'referrerPolicy',
|
||||
'rowSpan', 'srcDoc', 'srcLang', 'srcSet', 'useMap', 'fetchPriority',
|
||||
// SVG attributes
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
||||
'crossOrigin', 'accentHeight', 'alignmentBaseline', 'arabicForm', 'attributeName',
|
||||
'attributeType', 'baseFrequency', 'baselineShift', 'baseProfile', 'calcMode', 'capHeight',
|
||||
'clipPathUnits', 'clipPath', 'clipRule', 'colorInterpolation', 'colorInterpolationFilters',
|
||||
'colorProfile', 'colorRendering', 'contentScriptType', 'contentStyleType', 'diffuseConstant',
|
||||
'dominantBaseline', 'edgeMode', 'enableBackground', 'fillOpacity', 'fillRule', 'filterRes',
|
||||
'filterUnits', 'floodColor', 'floodOpacity', 'fontFamily', 'fontSize', 'fontSizeAdjust',
|
||||
'fontStretch', 'fontStyle', 'fontVariant', 'fontWeight', 'glyphName',
|
||||
'glyphOrientationHorizontal', 'glyphOrientationVertical', 'glyphRef', 'gradientTransform',
|
||||
'gradientUnits', 'horizAdvX', 'horizOriginX', 'imageRendering', 'kernelMatrix',
|
||||
'kernelUnitLength', 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'letterSpacing',
|
||||
'lightingColor', 'limitingConeAngle', 'markerEnd', 'markerMid', 'markerStart', 'markerHeight',
|
||||
'markerUnits', 'markerWidth', 'maskContentUnits', 'maskUnits', 'mathematical', 'numOctaves',
|
||||
'overlinePosition', 'overlineThickness', 'panose1', 'paintOrder', 'pathLength',
|
||||
'patternContentUnits', 'patternTransform', 'patternUnits', 'pointerEvents', 'pointsAtX',
|
||||
'pointsAtY', 'pointsAtZ', 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits',
|
||||
'referrerPolicy', 'refX', 'refY', 'rendering-intent', 'repeatCount', 'repeatDur',
|
||||
'requiredExtensions', 'requiredFeatures', 'shapeRendering', 'specularConstant',
|
||||
'specularExponent', 'spreadMethod', 'startOffset', 'stdDeviation', 'stitchTiles', 'stopColor',
|
||||
'stopOpacity', 'strikethroughPosition', 'strikethroughThickness', 'strokeDasharray',
|
||||
'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 'strokeOpacity',
|
||||
'strokeWidth', 'surfaceScale', 'systemLanguage', 'tableValues', 'targetX', 'targetY',
|
||||
'textAnchor', 'textDecoration', 'textRendering', 'textLength', 'transformOrigin',
|
||||
'underlinePosition', 'underlineThickness', 'unicodeBidi', 'unicodeRange', 'unitsPerEm',
|
||||
'vAlphabetic', 'vHanging', 'vIdeographic', 'vMathematical', 'vectorEffect', 'vertAdvY',
|
||||
'vertOriginX', 'vertOriginY', 'viewBox', 'viewTarget', 'wordSpacing', 'writingMode', 'xHeight',
|
||||
'xChannelSelector', 'xlinkActuate', 'xlinkArcrole', 'xlinkHref', 'xlinkRole', 'xlinkShow',
|
||||
'xlinkTitle', 'xlinkType', 'xmlBase', 'xmlLang', 'xmlnsXlink', 'xmlSpace', 'yChannelSelector',
|
||||
'zoomAndPan',
|
||||
// Safari/Apple specific, no listing available
|
||||
'autoCorrect', // https://stackoverflow.com/questions/47985384/html-autocorrect-for-text-input-is-not-working
|
||||
'autoSave', // https://stackoverflow.com/questions/25456396/what-is-autosave-attribute-supposed-to-do-how-do-i-use-it
|
||||
// React specific attributes https://reactjs.org/docs/dom-elements.html#differences-in-attributes
|
||||
'className', 'dangerouslySetInnerHTML', 'defaultValue', 'defaultChecked', 'htmlFor',
|
||||
// Events' capture events
|
||||
'onBeforeInput', 'onChange',
|
||||
'onInvalid', 'onReset', 'onTouchCancel', 'onTouchEnd', 'onTouchMove', 'onTouchStart', 'suppressContentEditableWarning', 'suppressHydrationWarning',
|
||||
'onAbort', 'onCanPlay', 'onCanPlayThrough', 'onDurationChange', 'onEmptied', 'onEncrypted', 'onEnded',
|
||||
'onLoadedData', 'onLoadedMetadata', 'onLoadStart', 'onPause', 'onPlay', 'onPlaying', 'onProgress', 'onRateChange', 'onResize',
|
||||
'onSeeked', 'onSeeking', 'onStalled', 'onSuspend', 'onTimeUpdate', 'onVolumeChange', 'onWaiting',
|
||||
'onCopyCapture', 'onCutCapture', 'onPasteCapture', 'onCompositionEndCapture', 'onCompositionStartCapture', 'onCompositionUpdateCapture',
|
||||
'onFocusCapture', 'onBlurCapture', 'onChangeCapture', 'onBeforeInputCapture', 'onInputCapture', 'onResetCapture', 'onSubmitCapture',
|
||||
'onInvalidCapture', 'onLoadCapture', 'onErrorCapture', 'onKeyDownCapture', 'onKeyPressCapture', 'onKeyUpCapture',
|
||||
'onAbortCapture', 'onCanPlayCapture', 'onCanPlayThroughCapture', 'onDurationChangeCapture', 'onEmptiedCapture', 'onEncryptedCapture',
|
||||
'onEndedCapture', 'onLoadedDataCapture', 'onLoadedMetadataCapture', 'onLoadStartCapture', 'onPauseCapture', 'onPlayCapture',
|
||||
'onPlayingCapture', 'onProgressCapture', 'onRateChangeCapture', 'onSeekedCapture', 'onSeekingCapture', 'onStalledCapture', 'onSuspendCapture',
|
||||
'onTimeUpdateCapture', 'onVolumeChangeCapture', 'onWaitingCapture', 'onSelectCapture', 'onTouchCancelCapture', 'onTouchEndCapture',
|
||||
'onTouchMoveCapture', 'onTouchStartCapture', 'onScrollCapture', 'onWheelCapture', 'onAnimationEndCapture', 'onAnimationIteration',
|
||||
'onAnimationStartCapture', 'onTransitionEndCapture',
|
||||
'onAuxClick', 'onAuxClickCapture', 'onClickCapture', 'onContextMenuCapture', 'onDoubleClickCapture',
|
||||
'onDragCapture', 'onDragEndCapture', 'onDragEnterCapture', 'onDragExitCapture', 'onDragLeaveCapture',
|
||||
'onDragOverCapture', 'onDragStartCapture', 'onDropCapture', 'onMouseDown', 'onMouseDownCapture',
|
||||
'onMouseMoveCapture', 'onMouseOutCapture', 'onMouseOverCapture', 'onMouseUpCapture',
|
||||
// Video specific
|
||||
'autoPictureInPicture', 'controlsList', 'disablePictureInPicture', 'disableRemotePlayback',
|
||||
];
|
||||
|
||||
const DOM_PROPERTIES_IGNORE_CASE = ['charset', 'allowFullScreen', 'webkitAllowFullScreen', 'mozAllowFullScreen', 'webkitDirectory'];
|
||||
|
||||
const ARIA_PROPERTIES = [
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
|
||||
// Global attributes
|
||||
'aria-atomic', 'aria-braillelabel', 'aria-brailleroledescription', 'aria-busy', 'aria-controls', 'aria-current',
|
||||
'aria-describedby', 'aria-description', 'aria-details',
|
||||
'aria-disabled', 'aria-dropeffect', 'aria-errormessage', 'aria-flowto', 'aria-grabbed', 'aria-haspopup',
|
||||
'aria-hidden', 'aria-invalid', 'aria-keyshortcuts', 'aria-label', 'aria-labelledby', 'aria-live',
|
||||
'aria-owns', 'aria-relevant', 'aria-roledescription',
|
||||
// Widget attributes
|
||||
'aria-autocomplete', 'aria-checked', 'aria-expanded', 'aria-level', 'aria-modal', 'aria-multiline', 'aria-multiselectable',
|
||||
'aria-orientation', 'aria-placeholder', 'aria-pressed', 'aria-readonly', 'aria-required', 'aria-selected',
|
||||
'aria-sort', 'aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext',
|
||||
// Relationship attributes
|
||||
'aria-activedescendant', 'aria-colcount', 'aria-colindex', 'aria-colindextext', 'aria-colspan',
|
||||
'aria-posinset', 'aria-rowcount', 'aria-rowindex', 'aria-rowindextext', 'aria-rowspan', 'aria-setsize',
|
||||
];
|
||||
|
||||
const REACT_ON_PROPS = [
|
||||
'onGotPointerCapture',
|
||||
'onGotPointerCaptureCapture',
|
||||
'onLostPointerCapture',
|
||||
'onLostPointerCapture',
|
||||
'onLostPointerCaptureCapture',
|
||||
'onPointerCancel',
|
||||
'onPointerCancelCapture',
|
||||
'onPointerDown',
|
||||
'onPointerDownCapture',
|
||||
'onPointerEnter',
|
||||
'onPointerEnterCapture',
|
||||
'onPointerLeave',
|
||||
'onPointerLeaveCapture',
|
||||
'onPointerMove',
|
||||
'onPointerMoveCapture',
|
||||
'onPointerOut',
|
||||
'onPointerOutCapture',
|
||||
'onPointerOver',
|
||||
'onPointerOverCapture',
|
||||
'onPointerUp',
|
||||
'onPointerUpCapture',
|
||||
];
|
||||
|
||||
function getDOMPropertyNames(context) {
|
||||
const ALL_DOM_PROPERTY_NAMES = DOM_PROPERTY_NAMES_TWO_WORDS.concat(DOM_PROPERTY_NAMES_ONE_WORD);
|
||||
// this was removed in React v16.1+, see https://github.com/facebook/react/pull/10823
|
||||
if (!testReactVersion(context, '>= 16.1.0')) {
|
||||
return ALL_DOM_PROPERTY_NAMES.concat('allowTransparency');
|
||||
}
|
||||
// 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
|
||||
if (testReactVersion(context, '>= 16.4.0')) {
|
||||
return ALL_DOM_PROPERTY_NAMES.concat(REACT_ON_PROPS);
|
||||
}
|
||||
return ALL_DOM_PROPERTY_NAMES;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks if a node's parent is a JSX tag that is written with lowercase letters,
|
||||
* and is not a custom web component. Custom web components have a hyphen in tag name,
|
||||
* or have an `is="some-elem"` attribute.
|
||||
*
|
||||
* Note: does not check if a tag's parent against a list of standard HTML/DOM tags. For example,
|
||||
* a `<fake>`'s child would return `true` because "fake" is written only with lowercase letters
|
||||
* without a hyphen and does not have a `is="some-elem"` attribute.
|
||||
*
|
||||
* @param {Object} childNode - JSX element being tested.
|
||||
* @returns {boolean} Whether or not the node name match the JSX tag convention.
|
||||
*/
|
||||
function isValidHTMLTagInJSX(childNode) {
|
||||
const tagConvention = /^[a-z][^-]*$/;
|
||||
if (tagConvention.test(childNode.parent.name.name)) {
|
||||
return !childNode.parent.attributes.some((attrNode) => (
|
||||
attrNode.type === 'JSXAttribute'
|
||||
&& attrNode.name.type === 'JSXIdentifier'
|
||||
&& attrNode.name.name === 'is'
|
||||
// To learn more about custom web components and `is` attribute,
|
||||
// see https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-customized-builtin-example
|
||||
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the attribute name is included in the attributes that are excluded
|
||||
* from the camel casing.
|
||||
*
|
||||
* // returns 'charSet'
|
||||
* @example normalizeAttributeCase('charset')
|
||||
*
|
||||
* Note - these exclusions are not made by React core team, but `eslint-plugin-react` community.
|
||||
*
|
||||
* @param {string} name - Attribute name to be normalized
|
||||
* @returns {string} Result
|
||||
*/
|
||||
function normalizeAttributeCase(name) {
|
||||
return DOM_PROPERTIES_IGNORE_CASE.find((element) => element.toLowerCase() === name.toLowerCase()) || name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an attribute name is a valid `data-*` attribute:
|
||||
* if the name starts with "data-" and has alphanumeric words (browsers require lowercase, but React and TS lowercase them),
|
||||
* not start with any casing of "xml", and separated by hyphens (-) (which is also called "kebab case" or "dash case"),
|
||||
* then the attribute is a valid data attribute.
|
||||
*
|
||||
* @param {string} name - Attribute name to be tested
|
||||
* @returns {boolean} Result
|
||||
*/
|
||||
function isValidDataAttribute(name) {
|
||||
return !/^data-xml/i.test(name) && /^data-[^:]*$/.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an attribute name has at least one uppercase characters
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {boolean} Result
|
||||
*/
|
||||
function hasUpperCaseCharacter(name) {
|
||||
return name.toLowerCase() !== name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an attribute name is a standard aria attribute by compering it to a list
|
||||
* of standard aria property names
|
||||
*
|
||||
* @param {string} name - Attribute name to be tested
|
||||
* @returns {boolean} Result
|
||||
*/
|
||||
|
||||
function isValidAriaAttribute(name) {
|
||||
return ARIA_PROPERTIES.some((element) => element === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
) {
|
||||
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 {object} 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[/** @type {keyof DOM_ATTRIBUTE_NAMES} */ (name)];
|
||||
}
|
||||
if (has(SVGDOM_ATTRIBUTE_NAMES, name)) {
|
||||
return SVGDOM_ATTRIBUTE_NAMES[/** @type {keyof 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
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
invalidPropOnTag: 'Invalid property \'{{name}}\' found on tag \'{{tagName}}\', but it is only allowed on: {{allowedTags}}',
|
||||
unknownPropWithStandardName: 'Unknown property \'{{name}}\' found, use \'{{standardName}}\' instead',
|
||||
unknownProp: 'Unknown property \'{{name}}\' found',
|
||||
dataLowercaseRequired: 'React does not recognize data-* props with uppercase characters on a DOM element. Found \'{{name}}\', use \'{{lowerCaseName}}\' instead',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of unknown DOM property',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('no-unknown-property'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
requireDataLowercase: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function getIgnoreConfig() {
|
||||
return (context.options[0] && context.options[0].ignore) || DEFAULTS.ignore;
|
||||
}
|
||||
|
||||
function getRequireDataLowercase() {
|
||||
return (context.options[0] && typeof context.options[0].requireDataLowercase !== 'undefined')
|
||||
? !!context.options[0].requireDataLowercase
|
||||
: DEFAULTS.requireDataLowercase;
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const ignoreNames = getIgnoreConfig();
|
||||
const actualName = getText(context, node.name);
|
||||
if (ignoreNames.indexOf(actualName) >= 0) {
|
||||
return;
|
||||
}
|
||||
const name = normalizeAttributeCase(actualName);
|
||||
|
||||
// Ignore tags like <Foo.bar />
|
||||
if (tagNameHasDot(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidDataAttribute(name)) {
|
||||
if (getRequireDataLowercase() && hasUpperCaseCharacter(name)) {
|
||||
report(context, messages.dataLowercaseRequired, 'dataLowercaseRequired', {
|
||||
node,
|
||||
data: {
|
||||
name: actualName,
|
||||
lowerCaseName: actualName.toLowerCase(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidAriaAttribute(name)) { return; }
|
||||
|
||||
const tagName = getTagName(node);
|
||||
|
||||
if (tagName === 'fbt' || tagName === 'fbs') { return; } // fbt/fbs nodes are bonkers, let's not go there
|
||||
|
||||
if (!isValidHTMLTagInJSX(node)) { return; }
|
||||
|
||||
// Let's dive deeper into tags that are HTML/DOM elements (`<button>`), and not React components (`<Button />`)
|
||||
|
||||
// Some attributes are allowed on some tags only
|
||||
const allowedTags = has(ATTRIBUTE_TAGS_MAP, name)
|
||||
? ATTRIBUTE_TAGS_MAP[/** @type {keyof ATTRIBUTE_TAGS_MAP} */ (name)]
|
||||
: null;
|
||||
if (tagName && allowedTags) {
|
||||
// Scenario 1A: Allowed attribute found where not supposed to, report it
|
||||
if (allowedTags.indexOf(tagName) === -1) {
|
||||
report(context, messages.invalidPropOnTag, 'invalidPropOnTag', {
|
||||
node,
|
||||
data: {
|
||||
name: actualName,
|
||||
tagName,
|
||||
allowedTags: allowedTags.join(', '),
|
||||
},
|
||||
});
|
||||
}
|
||||
// Scenario 1B: There are allowed attributes on allowed tags, no need to report it
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's see if the attribute is a close version to some standard property name
|
||||
const standardName = getStandardName(name, context);
|
||||
|
||||
const hasStandardNameButIsNotUsed = standardName && standardName !== name;
|
||||
const usesStandardName = standardName && standardName === name;
|
||||
|
||||
if (usesStandardName) {
|
||||
// Scenario 2A: The attribute name is the standard name, no need to report it
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasStandardNameButIsNotUsed) {
|
||||
// Scenario 2B: The name of the attribute is close to a standard one, report it with the standard name
|
||||
report(context, messages.unknownPropWithStandardName, 'unknownPropWithStandardName', {
|
||||
node,
|
||||
data: {
|
||||
name: actualName,
|
||||
standardName,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.name, standardName);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Scenario 3: We have an attribute that is unknown, report it
|
||||
report(context, messages.unknownProp, 'unknownProp', {
|
||||
node,
|
||||
data: {
|
||||
name: actualName,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
151
node_modules/eslint-plugin-react/lib/rules/no-unsafe.js
generated
vendored
Normal file
151
node_modules/eslint-plugin-react/lib/rules/no-unsafe.js
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of unsafe lifecycle methods
|
||||
* @author Sergei Startsev
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
unsafeMethod: '{{method}} is unsafe for use in async rendering. Update the component to use {{newMethod}} instead. {{details}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow usage of unsafe lifecycle methods',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unsafe'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkAliases: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const checkAliases = config.checkAliases || false;
|
||||
|
||||
const isApplicable = testReactVersion(context, '>= 16.3.0');
|
||||
if (!isApplicable) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const unsafe = {
|
||||
UNSAFE_componentWillMount: {
|
||||
newMethod: 'componentDidMount',
|
||||
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
|
||||
},
|
||||
UNSAFE_componentWillReceiveProps: {
|
||||
newMethod: 'getDerivedStateFromProps',
|
||||
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
|
||||
},
|
||||
UNSAFE_componentWillUpdate: {
|
||||
newMethod: 'componentDidUpdate',
|
||||
details: 'See https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html.',
|
||||
},
|
||||
};
|
||||
if (checkAliases) {
|
||||
unsafe.componentWillMount = unsafe.UNSAFE_componentWillMount;
|
||||
unsafe.componentWillReceiveProps = unsafe.UNSAFE_componentWillReceiveProps;
|
||||
unsafe.componentWillUpdate = unsafe.UNSAFE_componentWillUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of unsafe methods
|
||||
* @returns {Array} A list of unsafe methods
|
||||
*/
|
||||
function getUnsafeMethods() {
|
||||
return Object.keys(unsafe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a passed method is unsafe
|
||||
* @param {string} method Life cycle method
|
||||
* @returns {boolean} Returns true for unsafe methods, otherwise returns false
|
||||
*/
|
||||
function isUnsafe(method) {
|
||||
const unsafeMethods = getUnsafeMethods();
|
||||
return unsafeMethods.indexOf(method) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the error for an unsafe method
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @param {string} method Life cycle method
|
||||
*/
|
||||
function checkUnsafe(node, method) {
|
||||
if (!isUnsafe(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = unsafe[method];
|
||||
const newMethod = meta.newMethod;
|
||||
const details = meta.details;
|
||||
|
||||
const propertyNode = astUtil.getComponentProperties(node)
|
||||
.find((property) => astUtil.getPropertyName(property) === method);
|
||||
|
||||
report(context, messages.unsafeMethod, 'unsafeMethod', {
|
||||
node: propertyNode,
|
||||
data: {
|
||||
method,
|
||||
newMethod,
|
||||
details,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns life cycle methods if available
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {Array} The array of methods.
|
||||
*/
|
||||
function getLifeCycleMethods(node) {
|
||||
const properties = astUtil.getComponentProperties(node);
|
||||
return properties.map((property) => astUtil.getPropertyName(property));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks life cycle methods
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function checkLifeCycleMethods(node) {
|
||||
if (componentUtil.isES5Component(node, context) || componentUtil.isES6Component(node, context)) {
|
||||
const methods = getLifeCycleMethods(node);
|
||||
methods
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((method) => checkUnsafe(node, method));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: checkLifeCycleMethods,
|
||||
ClassExpression: checkLifeCycleMethods,
|
||||
ObjectExpression: checkLifeCycleMethods,
|
||||
};
|
||||
},
|
||||
};
|
||||
483
node_modules/eslint-plugin-react/lib/rules/no-unstable-nested-components.js
generated
vendored
Normal file
483
node_modules/eslint-plugin-react/lib/rules/no-unstable-nested-components.js
generated
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* @fileoverview Prevent creating unstable components inside components
|
||||
* @author Ari Perkkiö
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const isCreateElement = require('../util/isCreateElement');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.';
|
||||
const HOOK_REGEXP = /^use[A-Z0-9].*$/;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate error message with given parent component name
|
||||
* @param {string} parentName Name of the parent component, if known
|
||||
* @returns {string} Error message with parent component name
|
||||
*/
|
||||
function generateErrorMessageWithParentName(parentName) {
|
||||
return `Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component${parentName ? ` “${parentName}” ` : ' '}and pass data as props.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given text starts with `render`. Comparison is case-sensitive.
|
||||
* @param {string} text Text to validate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function startsWithRender(text) {
|
||||
return typeof text === 'string' && text.startsWith('render');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get closest parent matching given matcher
|
||||
* @param {ASTNode} node The AST node
|
||||
* @param {Context} context eslint context
|
||||
* @param {Function} matcher Method used to match the parent
|
||||
* @returns {ASTNode} The matching parent node, if any
|
||||
*/
|
||||
function getClosestMatchingParent(node, context, matcher) {
|
||||
if (!node || !node.parent || node.parent.type === 'Program') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matcher(node.parent, context)) {
|
||||
return node.parent;
|
||||
}
|
||||
|
||||
return getClosestMatchingParent(node.parent, context, matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher used to check whether given node is a `createElement` call
|
||||
* @param {ASTNode} node The AST node
|
||||
* @param {Context} context eslint context
|
||||
* @returns {boolean} True if node is a `createElement` call, false if not
|
||||
*/
|
||||
function isCreateElementMatcher(node, context) {
|
||||
return (
|
||||
astUtil.isCallExpression(node)
|
||||
&& isCreateElement(context, node)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher used to check whether given node is a `ObjectExpression`
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is a `ObjectExpression`, false if not
|
||||
*/
|
||||
function isObjectExpressionMatcher(node) {
|
||||
return node && node.type === 'ObjectExpression';
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher used to check whether given node is a `JSXExpressionContainer`
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is a `JSXExpressionContainer`, false if not
|
||||
*/
|
||||
function isJSXExpressionContainerMatcher(node) {
|
||||
return node && node.type === 'JSXExpressionContainer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher used to check whether given node is a `JSXAttribute` of `JSXExpressionContainer`
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is a `JSXAttribute` of `JSXExpressionContainer`, false if not
|
||||
*/
|
||||
function isJSXAttributeOfExpressionContainerMatcher(node) {
|
||||
return (
|
||||
node
|
||||
&& node.type === 'JSXAttribute'
|
||||
&& node.value
|
||||
&& node.value.type === 'JSXExpressionContainer'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher used to check whether given node is an object `Property`
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is a `Property`, false if not
|
||||
*/
|
||||
function isPropertyOfObjectExpressionMatcher(node) {
|
||||
return (
|
||||
node
|
||||
&& node.parent
|
||||
&& node.parent.type === 'Property'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node or its parent is directly inside `map` call
|
||||
* ```jsx
|
||||
* {items.map(item => <li />)}
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is directly inside `map` call, false if not
|
||||
*/
|
||||
function isMapCall(node) {
|
||||
return (
|
||||
node
|
||||
&& node.callee
|
||||
&& node.callee.property
|
||||
&& node.callee.property.name === 'map'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is `ReturnStatement` of a React hook
|
||||
* @param {ASTNode} node The AST node
|
||||
* @param {Context} context eslint context
|
||||
* @returns {boolean} True if node is a `ReturnStatement` of a React hook, false if not
|
||||
*/
|
||||
function isReturnStatementOfHook(node, context) {
|
||||
if (
|
||||
!node
|
||||
|| !node.parent
|
||||
|| node.parent.type !== 'ReturnStatement'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callExpression = getClosestMatchingParent(node, context, astUtil.isCallExpression);
|
||||
return (
|
||||
callExpression
|
||||
&& callExpression.callee
|
||||
&& HOOK_REGEXP.test(callExpression.callee.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is declared inside a render prop
|
||||
* ```jsx
|
||||
* <Component renderFooter={() => <div />} />
|
||||
* <Component>{() => <div />}</Component>
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node
|
||||
* @param {Context} context eslint context
|
||||
* @returns {boolean} True if component is declared inside a render prop, false if not
|
||||
*/
|
||||
function isComponentInRenderProp(node, context) {
|
||||
if (
|
||||
node
|
||||
&& node.parent
|
||||
&& node.parent.type === 'Property'
|
||||
&& node.parent.key
|
||||
&& startsWithRender(node.parent.key.name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check whether component is a render prop used as direct children, e.g. <Component>{() => <div />}</Component>
|
||||
if (
|
||||
node
|
||||
&& node.parent
|
||||
&& node.parent.type === 'JSXExpressionContainer'
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.type === 'JSXElement'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jsxExpressionContainer = getClosestMatchingParent(node, context, isJSXExpressionContainerMatcher);
|
||||
|
||||
// Check whether prop name indicates accepted patterns
|
||||
if (
|
||||
jsxExpressionContainer
|
||||
&& jsxExpressionContainer.parent
|
||||
&& jsxExpressionContainer.parent.type === 'JSXAttribute'
|
||||
&& jsxExpressionContainer.parent.name
|
||||
&& jsxExpressionContainer.parent.name.type === 'JSXIdentifier'
|
||||
) {
|
||||
const propName = jsxExpressionContainer.parent.name.name;
|
||||
|
||||
// Starts with render, e.g. <Component renderFooter={() => <div />} />
|
||||
if (startsWithRender(propName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Uses children prop explicitly, e.g. <Component children={() => <div />} />
|
||||
if (propName === 'children') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is declared directly inside a render property
|
||||
* ```jsx
|
||||
* const rows = { render: () => <div /> }
|
||||
* <Component rows={ [{ render: () => <div /> }] } />
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if component is declared inside a render property, false if not
|
||||
*/
|
||||
function isDirectValueOfRenderProperty(node) {
|
||||
return (
|
||||
node
|
||||
&& node.parent
|
||||
&& node.parent.type === 'Property'
|
||||
&& node.parent.key
|
||||
&& node.parent.key.type === 'Identifier'
|
||||
&& startsWithRender(node.parent.key.name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the component name of given node
|
||||
* @param {ASTNode} node The AST node of the component
|
||||
* @returns {string} Name of the component, if any
|
||||
*/
|
||||
function resolveComponentName(node) {
|
||||
const parentName = node.id && node.id.name;
|
||||
if (parentName) return parentName;
|
||||
|
||||
return (
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
&& node.parent
|
||||
&& node.parent.id
|
||||
&& node.parent.id.name
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow creating unstable components inside components',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unstable-nested-components'),
|
||||
},
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
customValidators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
allowAsProps: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const allowAsProps = context.options.some((option) => option && option.allowAsProps);
|
||||
|
||||
/**
|
||||
* Check whether given node is declared inside class component's render block
|
||||
* ```jsx
|
||||
* class Component extends React.Component {
|
||||
* render() {
|
||||
* class NestedClassComponent extends React.Component {
|
||||
* ...
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {boolean} True if node is inside class component's render block, false if not
|
||||
*/
|
||||
function isInsideRenderMethod(node) {
|
||||
const parentComponent = utils.getParentComponent(node);
|
||||
|
||||
if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
node
|
||||
&& node.parent
|
||||
&& node.parent.type === 'MethodDefinition'
|
||||
&& node.parent.key
|
||||
&& node.parent.key.name === 'render'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is a function component declared inside class component.
|
||||
* Util's component detection fails to detect function components inside class components.
|
||||
* ```jsx
|
||||
* class Component extends React.Component {
|
||||
* render() {
|
||||
* const NestedComponent = () => <div />;
|
||||
* ...
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {boolean} True if given node a function component declared inside class component, false if not
|
||||
*/
|
||||
function isFunctionComponentInsideClassComponent(node) {
|
||||
const parentComponent = utils.getParentComponent(node);
|
||||
const parentStatelessComponent = utils.getParentStatelessComponent(node);
|
||||
|
||||
return (
|
||||
parentComponent
|
||||
&& parentStatelessComponent
|
||||
&& parentComponent.type === 'ClassDeclaration'
|
||||
&& utils.getStatelessComponent(parentStatelessComponent)
|
||||
&& utils.isReturningJSX(node)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is declared inside `createElement` call's props
|
||||
* ```js
|
||||
* React.createElement(Component, {
|
||||
* footer: () => React.createElement("div", null)
|
||||
* })
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node
|
||||
* @returns {boolean} True if node is declare inside `createElement` call's props, false if not
|
||||
*/
|
||||
function isComponentInsideCreateElementsProp(node) {
|
||||
if (!components.get(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const createElementParent = getClosestMatchingParent(node, context, isCreateElementMatcher);
|
||||
|
||||
return (
|
||||
createElementParent
|
||||
&& createElementParent.arguments
|
||||
&& createElementParent.arguments[1] === getClosestMatchingParent(node, context, isObjectExpressionMatcher)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is declared inside a component/object prop.
|
||||
* ```jsx
|
||||
* <Component footer={() => <div />} />
|
||||
* { footer: () => <div /> }
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {boolean} True if node is a component declared inside prop, false if not
|
||||
*/
|
||||
function isComponentInProp(node) {
|
||||
if (isPropertyOfObjectExpressionMatcher(node)) {
|
||||
return utils.isReturningJSX(node);
|
||||
}
|
||||
|
||||
const jsxAttribute = getClosestMatchingParent(node, context, isJSXAttributeOfExpressionContainerMatcher);
|
||||
|
||||
if (!jsxAttribute) {
|
||||
return isComponentInsideCreateElementsProp(node);
|
||||
}
|
||||
|
||||
return utils.isReturningJSX(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is a stateless component returning non-JSX
|
||||
* ```jsx
|
||||
* {{ a: () => null }}
|
||||
* ```
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
* @returns {boolean} True if node is a stateless component returning non-JSX, false if not
|
||||
*/
|
||||
function isStatelessComponentReturningNull(node) {
|
||||
const component = utils.getStatelessComponent(node);
|
||||
|
||||
return component && !utils.isReturningJSX(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given node is a unstable nested component
|
||||
* @param {ASTNode} node The AST node being checked
|
||||
*/
|
||||
function validate(node) {
|
||||
if (!node || !node.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeclaredInsideProps = isComponentInProp(node);
|
||||
|
||||
if (
|
||||
!components.get(node)
|
||||
&& !isFunctionComponentInsideClassComponent(node)
|
||||
&& !isDeclaredInsideProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
// Support allowAsProps option
|
||||
(isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context)))
|
||||
|
||||
// Prevent reporting components created inside Array.map calls
|
||||
|| isMapCall(node)
|
||||
|| isMapCall(node.parent)
|
||||
|
||||
// Do not mark components declared inside hooks (or falsy '() => null' clean-up methods)
|
||||
|| isReturnStatementOfHook(node, context)
|
||||
|
||||
// Do not mark objects containing render methods
|
||||
|| isDirectValueOfRenderProperty(node)
|
||||
|
||||
// Prevent reporting nested class components twice
|
||||
|| isInsideRenderMethod(node)
|
||||
|
||||
// Prevent falsely reporting detected "components" which do not return JSX
|
||||
|| isStatelessComponentReturningNull(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the closest parent component
|
||||
const parentComponent = getClosestMatchingParent(
|
||||
node,
|
||||
context,
|
||||
(nodeToMatch) => components.get(nodeToMatch)
|
||||
);
|
||||
|
||||
if (parentComponent) {
|
||||
const parentName = resolveComponentName(parentComponent);
|
||||
|
||||
// Exclude lowercase parents, e.g. function createTestComponent()
|
||||
// React-dom prevents creating lowercase components
|
||||
if (parentName && parentName[0] === parentName[0].toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = generateErrorMessageWithParentName(parentName);
|
||||
|
||||
// Add information about allowAsProps option when component is declared inside prop
|
||||
if (isDeclaredInsideProps && !allowAsProps) {
|
||||
message += COMPONENT_AS_PROPS_INFO;
|
||||
}
|
||||
|
||||
report(context, message, null, {
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
FunctionDeclaration(node) { validate(node); },
|
||||
ArrowFunctionExpression(node) { validate(node); },
|
||||
FunctionExpression(node) { validate(node); },
|
||||
ClassDeclaration(node) { validate(node); },
|
||||
CallExpression(node) { validate(node); },
|
||||
};
|
||||
}),
|
||||
};
|
||||
258
node_modules/eslint-plugin-react/lib/rules/no-unused-class-component-methods.js
generated
vendored
Normal file
258
node_modules/eslint-plugin-react/lib/rules/no-unused-class-component-methods.js
generated
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* @fileoverview Prevent declaring unused methods and properties of component class
|
||||
* @author Paweł Nowak, Berton Zhu
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const LIFECYCLE_METHODS = new Set([
|
||||
'constructor',
|
||||
'componentDidCatch',
|
||||
'componentDidMount',
|
||||
'componentDidUpdate',
|
||||
'componentWillMount',
|
||||
'componentWillReceiveProps',
|
||||
'componentWillUnmount',
|
||||
'componentWillUpdate',
|
||||
'getChildContext',
|
||||
'getSnapshotBeforeUpdate',
|
||||
'render',
|
||||
'shouldComponentUpdate',
|
||||
'UNSAFE_componentWillMount',
|
||||
'UNSAFE_componentWillReceiveProps',
|
||||
'UNSAFE_componentWillUpdate',
|
||||
]);
|
||||
|
||||
const ES6_LIFECYCLE = new Set([
|
||||
'state',
|
||||
]);
|
||||
|
||||
const ES5_LIFECYCLE = new Set([
|
||||
'getInitialState',
|
||||
'getDefaultProps',
|
||||
'mixins',
|
||||
]);
|
||||
|
||||
function isKeyLiteralLike(node, property) {
|
||||
return property.type === 'Literal'
|
||||
|| (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|
||||
|| (node.computed === false && property.type === 'Identifier');
|
||||
}
|
||||
|
||||
// Descend through all wrapping TypeCastExpressions and return the expression
|
||||
// that was cast.
|
||||
function uncast(node) {
|
||||
while (node.type === 'TypeCastExpression') {
|
||||
node = node.expression;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Return the name of an identifier or the string value of a literal. Useful
|
||||
// anywhere that a literal may be used as a key (e.g., member expressions,
|
||||
// method definitions, ObjectExpression property keys).
|
||||
function getName(node) {
|
||||
node = uncast(node);
|
||||
const type = node.type;
|
||||
|
||||
if (type === 'Identifier') {
|
||||
return node.name;
|
||||
}
|
||||
if (type === 'Literal') {
|
||||
return String(node.value);
|
||||
}
|
||||
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
|
||||
return node.quasis[0].value.raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isThisExpression(node) {
|
||||
return uncast(node).type === 'ThisExpression';
|
||||
}
|
||||
|
||||
function getInitialClassInfo(node, isClass) {
|
||||
return {
|
||||
classNode: node,
|
||||
isClass,
|
||||
// Set of nodes where properties were defined.
|
||||
properties: new Set(),
|
||||
|
||||
// Set of names of properties that we've seen used.
|
||||
usedProperties: new Set(),
|
||||
|
||||
inStatic: false,
|
||||
};
|
||||
}
|
||||
|
||||
const messages = {
|
||||
unused: 'Unused method or property "{{name}}"',
|
||||
unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow declaring unused methods of component class',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unused-class-component-methods'),
|
||||
},
|
||||
messages,
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: ((context) => {
|
||||
let classInfo = null;
|
||||
|
||||
// Takes an ObjectExpression node and adds all named Property nodes to the
|
||||
// current set of properties.
|
||||
function addProperty(node) {
|
||||
classInfo.properties.add(node);
|
||||
}
|
||||
|
||||
// Adds the name of the given node as a used property if the node is an
|
||||
// Identifier or a Literal. Other node types are ignored.
|
||||
function addUsedProperty(node) {
|
||||
const name = getName(node);
|
||||
if (name) {
|
||||
classInfo.usedProperties.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
function reportUnusedProperties() {
|
||||
// Report all unused properties.
|
||||
for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
|
||||
const name = getName(node);
|
||||
if (
|
||||
!classInfo.usedProperties.has(name)
|
||||
&& !LIFECYCLE_METHODS.has(name)
|
||||
&& (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
|
||||
) {
|
||||
const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
|
||||
|
||||
const messageID = className ? 'unusedWithClass' : 'unused';
|
||||
report(
|
||||
context,
|
||||
messages[messageID],
|
||||
messageID,
|
||||
{
|
||||
node,
|
||||
data: {
|
||||
name,
|
||||
className,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exitMethod() {
|
||||
if (!classInfo || !classInfo.inStatic) {
|
||||
return;
|
||||
}
|
||||
|
||||
classInfo.inStatic = false;
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
if (componentUtil.isES6Component(node, context)) {
|
||||
classInfo = getInitialClassInfo(node, true);
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (componentUtil.isES5Component(node, context)) {
|
||||
classInfo = getInitialClassInfo(node, false);
|
||||
}
|
||||
},
|
||||
|
||||
'ClassDeclaration:exit'() {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
reportUnusedProperties();
|
||||
classInfo = null;
|
||||
},
|
||||
|
||||
'ObjectExpression:exit'(node) {
|
||||
if (!classInfo || classInfo.classNode !== node) {
|
||||
return;
|
||||
}
|
||||
reportUnusedProperties();
|
||||
classInfo = null;
|
||||
},
|
||||
|
||||
Property(node) {
|
||||
if (!classInfo || classInfo.classNode !== node.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isKeyLiteralLike(node, node.key)) {
|
||||
addProperty(node.key);
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.static) {
|
||||
classInfo.inStatic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isKeyLiteralLike(node, node.key)) {
|
||||
addProperty(node.key);
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty:exit': exitMethod,
|
||||
'MethodDefinition:exit': exitMethod,
|
||||
'PropertyDefinition:exit': exitMethod,
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!classInfo || classInfo.inStatic) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
|
||||
if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
|
||||
// detect `this.property = xxx`
|
||||
addProperty(node.property);
|
||||
} else {
|
||||
// detect `this.property()`, `x = this.property`, etc.
|
||||
addUsedProperty(node.property);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (!classInfo || classInfo.inStatic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// detect `{ foo, bar: baz } = this`
|
||||
if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
|
||||
node.id.properties
|
||||
.filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
|
||||
.forEach((prop) => {
|
||||
addUsedProperty('key' in prop ? prop.key : undefined);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
171
node_modules/eslint-plugin-react/lib/rules/no-unused-prop-types.js
generated
vendored
Normal file
171
node_modules/eslint-plugin-react/lib/rules/no-unused-prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @fileoverview Prevent definitions of unused prop types
|
||||
* @author Evgueni Naverniouk
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
// As for exceptions for props.children or props.className (and alike) look at
|
||||
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
/**
|
||||
* Checks if the component must be validated
|
||||
* @param {Object} component The component to process
|
||||
* @returns {boolean} True if the component must be validated, false if not.
|
||||
*/
|
||||
function mustBeValidated(component) {
|
||||
return !!component && !component.ignoreUnusedPropTypesValidation;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
unusedPropType: '\'{{name}}\' PropType is defined but prop is never used',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow definitions of unused propTypes',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unused-prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
customValidators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
skipShapeProps: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const defaults = { skipShapeProps: true, customValidators: [], ignore: [] };
|
||||
const configuration = Object.assign({}, defaults, context.options[0] || {});
|
||||
|
||||
/**
|
||||
* Checks if the prop is ignored
|
||||
* @param {string} name Name of the prop to check.
|
||||
* @returns {boolean} True if the prop is ignored, false if not.
|
||||
*/
|
||||
function isIgnored(name) {
|
||||
return configuration.ignore.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a prop is used
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {Object} prop Declared prop object
|
||||
* @returns {boolean} True if the prop is used, false if not.
|
||||
*/
|
||||
function isPropUsed(node, prop) {
|
||||
const usedPropTypes = node.usedPropTypes || [];
|
||||
for (let i = 0, l = usedPropTypes.length; i < l; i++) {
|
||||
const usedProp = usedPropTypes[i];
|
||||
if (
|
||||
prop.type === 'shape'
|
||||
|| prop.type === 'exact'
|
||||
|| prop.name === '__ANY_KEY__'
|
||||
|| usedProp.name === prop.name
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to recursively loop through each declared prop type
|
||||
* @param {Object} component The component to process
|
||||
* @param {ASTNode[]|true} props List of props to validate
|
||||
*/
|
||||
function reportUnusedPropType(component, props) {
|
||||
// Skip props that check instances
|
||||
if (props === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(props || {}).forEach((key) => {
|
||||
const prop = props[key];
|
||||
// Skip props that check instances
|
||||
if (prop === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((prop.type === 'shape' || prop.type === 'exact') && configuration.skipShapeProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop.node && prop.node.typeAnnotation && prop.node.typeAnnotation.typeAnnotation
|
||||
&& prop.node.typeAnnotation.typeAnnotation.type === 'TSNeverKeyword') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prop.node && !isIgnored(prop.fullName) && !isPropUsed(component, prop)) {
|
||||
report(context, messages.unusedPropType, 'unusedPropType', {
|
||||
node: prop.node.key || prop.node,
|
||||
data: {
|
||||
name: prop.fullName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (prop.children) {
|
||||
reportUnusedPropType(component, prop.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports unused proptypes for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportUnusedPropTypes(component) {
|
||||
reportUnusedPropType(component, component.declaredPropTypes);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
// Report undeclared proptypes for all classes
|
||||
values(components.list())
|
||||
.filter((component) => mustBeValidated(component))
|
||||
.forEach((component) => {
|
||||
reportUnusedPropTypes(component);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
529
node_modules/eslint-plugin-react/lib/rules/no-unused-state.js
generated
vendored
Normal file
529
node_modules/eslint-plugin-react/lib/rules/no-unused-state.js
generated
vendored
Normal file
@@ -0,0 +1,529 @@
|
||||
/**
|
||||
* @fileoverview Attempts to discover all state fields in a React component and
|
||||
* warn if any of them are never read.
|
||||
*
|
||||
* State field definitions are collected from `this.state = {}` assignments in
|
||||
* the constructor, objects passed to `this.setState()`, and `state = {}` class
|
||||
* property assignments.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const report = require('../util/report');
|
||||
const getScope = require('../util/eslint').getScope;
|
||||
|
||||
// Descend through all wrapping TypeCastExpressions and return the expression
|
||||
// that was cast.
|
||||
function uncast(node) {
|
||||
while (node.type === 'TypeCastExpression') {
|
||||
node = node.expression;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Return the name of an identifier or the string value of a literal. Useful
|
||||
// anywhere that a literal may be used as a key (e.g., member expressions,
|
||||
// method definitions, ObjectExpression property keys).
|
||||
function getName(node) {
|
||||
node = uncast(node);
|
||||
const type = node.type;
|
||||
|
||||
if (type === 'Identifier') {
|
||||
return node.name;
|
||||
}
|
||||
if (type === 'Literal') {
|
||||
return String(node.value);
|
||||
}
|
||||
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
|
||||
return node.quasis[0].value.raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isThisExpression(node) {
|
||||
return astUtil.unwrapTSAsExpression(uncast(node)).type === 'ThisExpression';
|
||||
}
|
||||
|
||||
function getInitialClassInfo() {
|
||||
return {
|
||||
// Set of nodes where state fields were defined.
|
||||
stateFields: new Set(),
|
||||
|
||||
// Set of names of state fields that we've seen used.
|
||||
usedStateFields: new Set(),
|
||||
|
||||
// Names of local variables that may be pointing to this.state. To
|
||||
// track this properly, we would need to keep track of all locals,
|
||||
// shadowing, assignments, etc. To keep things simple, we only
|
||||
// maintain one set of aliases per method and accept that it will
|
||||
// produce some false negatives.
|
||||
aliases: null,
|
||||
};
|
||||
}
|
||||
|
||||
function isSetStateCall(node) {
|
||||
const unwrappedCalleeNode = astUtil.unwrapTSAsExpression(node.callee);
|
||||
|
||||
return (
|
||||
unwrappedCalleeNode.type === 'MemberExpression'
|
||||
&& isThisExpression(unwrappedCalleeNode.object)
|
||||
&& getName(unwrappedCalleeNode.property) === 'setState'
|
||||
);
|
||||
}
|
||||
|
||||
const messages = {
|
||||
unusedStateField: 'Unused state field: \'{{name}}\'',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow definitions of unused state',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('no-unused-state'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
// Non-null when we are inside a React component ClassDeclaration and we have
|
||||
// not yet encountered any use of this.state which we have chosen not to
|
||||
// analyze. If we encounter any such usage (like this.state being spread as
|
||||
// JSX attributes), then this is again set to null.
|
||||
let classInfo = null;
|
||||
|
||||
function isStateParameterReference(node) {
|
||||
const classMethods = [
|
||||
'shouldComponentUpdate',
|
||||
'componentWillUpdate',
|
||||
'UNSAFE_componentWillUpdate',
|
||||
'getSnapshotBeforeUpdate',
|
||||
'componentDidUpdate',
|
||||
];
|
||||
|
||||
let scope = getScope(context, node);
|
||||
while (scope) {
|
||||
const parent = scope.block && scope.block.parent;
|
||||
if (
|
||||
parent
|
||||
&& parent.type === 'MethodDefinition' && (
|
||||
(parent.static && parent.key.name === 'getDerivedStateFromProps')
|
||||
|| classMethods.indexOf(parent.key.name) !== -1
|
||||
)
|
||||
&& parent.value.type === 'FunctionExpression'
|
||||
&& parent.value.params[1]
|
||||
&& parent.value.params[1].name === node.name
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the given node is possibly a reference to `this.state` or the state parameter of
|
||||
// a lifecycle method.
|
||||
function isStateReference(node) {
|
||||
node = uncast(node);
|
||||
|
||||
const isDirectStateReference = node.type === 'MemberExpression'
|
||||
&& isThisExpression(node.object)
|
||||
&& node.property.name === 'state';
|
||||
|
||||
const isAliasedStateReference = node.type === 'Identifier'
|
||||
&& classInfo.aliases
|
||||
&& classInfo.aliases.has(node.name);
|
||||
|
||||
return isDirectStateReference || isAliasedStateReference || isStateParameterReference(node);
|
||||
}
|
||||
|
||||
// Takes an ObjectExpression node and adds all named Property nodes to the
|
||||
// current set of state fields.
|
||||
function addStateFields(node) {
|
||||
node.properties.filter((prop) => (
|
||||
prop.type === 'Property'
|
||||
&& (prop.key.type === 'Literal'
|
||||
|| (prop.key.type === 'TemplateLiteral' && prop.key.expressions.length === 0)
|
||||
|| (prop.computed === false && prop.key.type === 'Identifier'))
|
||||
&& getName(prop.key) !== null
|
||||
)).forEach((prop) => {
|
||||
classInfo.stateFields.add(prop);
|
||||
});
|
||||
}
|
||||
|
||||
// Adds the name of the given node as a used state field if the node is an
|
||||
// Identifier or a Literal. Other node types are ignored.
|
||||
function addUsedStateField(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
const name = getName(node);
|
||||
if (name) {
|
||||
classInfo.usedStateFields.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Records used state fields and new aliases for an ObjectPattern which
|
||||
// destructures `this.state`.
|
||||
function handleStateDestructuring(node) {
|
||||
node.properties.forEach((prop) => {
|
||||
if (prop.type === 'Property') {
|
||||
addUsedStateField(prop.key);
|
||||
} else if (
|
||||
(prop.type === 'ExperimentalRestProperty' || prop.type === 'RestElement')
|
||||
&& classInfo.aliases
|
||||
) {
|
||||
classInfo.aliases.add(getName(prop.argument));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Used to record used state fields and new aliases for both
|
||||
// AssignmentExpressions and VariableDeclarators.
|
||||
function handleAssignment(left, right) {
|
||||
const unwrappedRight = astUtil.unwrapTSAsExpression(right);
|
||||
|
||||
switch (left.type) {
|
||||
case 'Identifier':
|
||||
if (isStateReference(unwrappedRight) && classInfo.aliases) {
|
||||
classInfo.aliases.add(left.name);
|
||||
}
|
||||
break;
|
||||
case 'ObjectPattern':
|
||||
if (isStateReference(unwrappedRight)) {
|
||||
handleStateDestructuring(left);
|
||||
} else if (isThisExpression(unwrappedRight) && classInfo.aliases) {
|
||||
left.properties.forEach((prop) => {
|
||||
if (prop.type === 'Property' && getName(prop.key) === 'state') {
|
||||
const name = getName(prop.value);
|
||||
if (name) {
|
||||
classInfo.aliases.add(name);
|
||||
} else if (prop.value.type === 'ObjectPattern') {
|
||||
handleStateDestructuring(prop.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
function reportUnusedFields() {
|
||||
// Report all unused state fields.
|
||||
classInfo.stateFields.forEach((node) => {
|
||||
const name = getName(node.key);
|
||||
if (!classInfo.usedStateFields.has(name)) {
|
||||
report(context, messages.unusedStateField, 'unusedStateField', {
|
||||
node,
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleES6ComponentEnter(node) {
|
||||
if (componentUtil.isES6Component(node, context)) {
|
||||
classInfo = getInitialClassInfo();
|
||||
}
|
||||
}
|
||||
|
||||
function handleES6ComponentExit() {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
reportUnusedFields();
|
||||
classInfo = null;
|
||||
}
|
||||
|
||||
function isGDSFP(node) {
|
||||
const name = getName(node.key);
|
||||
if (
|
||||
!node.static
|
||||
|| name !== 'getDerivedStateFromProps'
|
||||
|| !node.value
|
||||
|| !node.value.params
|
||||
|| node.value.params.length < 2 // no `state` argument
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: handleES6ComponentEnter,
|
||||
|
||||
'ClassDeclaration:exit': handleES6ComponentExit,
|
||||
|
||||
ClassExpression: handleES6ComponentEnter,
|
||||
|
||||
'ClassExpression:exit': handleES6ComponentExit,
|
||||
|
||||
ObjectExpression(node) {
|
||||
if (componentUtil.isES5Component(node, context)) {
|
||||
classInfo = getInitialClassInfo();
|
||||
}
|
||||
},
|
||||
|
||||
'ObjectExpression:exit'(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (componentUtil.isES5Component(node, context)) {
|
||||
reportUnusedFields();
|
||||
classInfo = null;
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unwrappedNode = astUtil.unwrapTSAsExpression(node);
|
||||
const unwrappedArgumentNode = astUtil.unwrapTSAsExpression(unwrappedNode.arguments[0]);
|
||||
|
||||
// If we're looking at a `this.setState({})` invocation, record all the
|
||||
// properties as state fields.
|
||||
if (
|
||||
isSetStateCall(unwrappedNode)
|
||||
&& unwrappedNode.arguments.length > 0
|
||||
&& unwrappedArgumentNode.type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(unwrappedArgumentNode);
|
||||
} else if (
|
||||
isSetStateCall(unwrappedNode)
|
||||
&& unwrappedNode.arguments.length > 0
|
||||
&& unwrappedArgumentNode.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
const unwrappedBodyNode = astUtil.unwrapTSAsExpression(unwrappedArgumentNode.body);
|
||||
|
||||
if (unwrappedBodyNode.type === 'ObjectExpression') {
|
||||
addStateFields(unwrappedBodyNode);
|
||||
}
|
||||
if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) {
|
||||
const firstParam = unwrappedArgumentNode.params[0];
|
||||
if (firstParam.type === 'ObjectPattern') {
|
||||
handleStateDestructuring(firstParam);
|
||||
} else {
|
||||
classInfo.aliases.add(getName(firstParam));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
// If we see state being assigned as a class property using an object
|
||||
// expression, record all the fields of that object as state fields.
|
||||
const unwrappedValueNode = astUtil.unwrapTSAsExpression(node.value);
|
||||
|
||||
const name = getName(node.key);
|
||||
if (
|
||||
name === 'state'
|
||||
&& !node.static
|
||||
&& unwrappedValueNode
|
||||
&& unwrappedValueNode.type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(unwrappedValueNode);
|
||||
}
|
||||
|
||||
if (
|
||||
!node.static
|
||||
&& unwrappedValueNode
|
||||
&& unwrappedValueNode.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
// Create a new set for this.state aliases local to this method.
|
||||
classInfo.aliases = new Set();
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty:exit'(node) {
|
||||
if (
|
||||
classInfo
|
||||
&& !node.static
|
||||
&& node.value
|
||||
&& node.value.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
// Forget our set of local aliases.
|
||||
classInfo.aliases = null;
|
||||
}
|
||||
},
|
||||
|
||||
'PropertyDefinition, ClassProperty'(node) {
|
||||
if (!isGDSFP(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childScope = getScope(context, node).childScopes.find((x) => x.block === node.value);
|
||||
if (!childScope) {
|
||||
return;
|
||||
}
|
||||
const scope = childScope.variableScope.childScopes.find((x) => x.block === node.value);
|
||||
const stateArg = node.value.params[1]; // probably "state"
|
||||
if (!scope || !scope.variables) {
|
||||
return;
|
||||
}
|
||||
const argVar = scope.variables.find((x) => x.name === stateArg.name);
|
||||
|
||||
if (argVar) {
|
||||
const stateRefs = argVar.references;
|
||||
|
||||
stateRefs.forEach((ref) => {
|
||||
const identifier = ref.identifier;
|
||||
if (identifier && identifier.parent && identifier.parent.type === 'MemberExpression') {
|
||||
addUsedStateField(identifier.parent.property);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'PropertyDefinition:exit'(node) {
|
||||
if (
|
||||
classInfo
|
||||
&& !node.static
|
||||
&& node.value
|
||||
&& node.value.type === 'ArrowFunctionExpression'
|
||||
&& !isGDSFP(node)
|
||||
) {
|
||||
// Forget our set of local aliases.
|
||||
classInfo.aliases = null;
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition() {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
// Create a new set for this.state aliases local to this method.
|
||||
classInfo.aliases = new Set();
|
||||
},
|
||||
|
||||
'MethodDefinition:exit'() {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
// Forget our set of local aliases.
|
||||
classInfo.aliases = null;
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = node.parent;
|
||||
if (!componentUtil.isES5Component(parent.parent, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
'key' in parent
|
||||
&& 'name' in parent.key
|
||||
&& parent.key.name === 'getInitialState'
|
||||
) {
|
||||
const body = node.body.body;
|
||||
const lastBodyNode = body[body.length - 1];
|
||||
|
||||
if (
|
||||
lastBodyNode.type === 'ReturnStatement'
|
||||
&& lastBodyNode.argument.type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(lastBodyNode.argument);
|
||||
}
|
||||
} else {
|
||||
// Create a new set for this.state aliases local to this method.
|
||||
classInfo.aliases = new Set();
|
||||
}
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unwrappedLeft = astUtil.unwrapTSAsExpression(node.left);
|
||||
const unwrappedRight = astUtil.unwrapTSAsExpression(node.right);
|
||||
|
||||
// Check for assignments like `this.state = {}`
|
||||
if (
|
||||
unwrappedLeft.type === 'MemberExpression'
|
||||
&& isThisExpression(unwrappedLeft.object)
|
||||
&& getName(unwrappedLeft.property) === 'state'
|
||||
&& unwrappedRight.type === 'ObjectExpression'
|
||||
) {
|
||||
// Find the nearest function expression containing this assignment.
|
||||
/** @type {import("eslint").Rule.Node} */
|
||||
let fn = node;
|
||||
while (fn.type !== 'FunctionExpression' && fn.parent) {
|
||||
fn = fn.parent;
|
||||
}
|
||||
// If the nearest containing function is the constructor, then we want
|
||||
// to record all the assigned properties as state fields.
|
||||
if (
|
||||
fn.parent
|
||||
&& fn.parent.type === 'MethodDefinition'
|
||||
&& fn.parent.kind === 'constructor'
|
||||
) {
|
||||
addStateFields(unwrappedRight);
|
||||
}
|
||||
} else {
|
||||
// Check for assignments like `alias = this.state` and record the alias.
|
||||
handleAssignment(unwrappedLeft, unwrappedRight);
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (!classInfo || !node.init) {
|
||||
return;
|
||||
}
|
||||
handleAssignment(node.id, node.init);
|
||||
},
|
||||
|
||||
'MemberExpression, OptionalMemberExpression'(node) {
|
||||
if (!classInfo) {
|
||||
return;
|
||||
}
|
||||
if (isStateReference(astUtil.unwrapTSAsExpression(node.object))) {
|
||||
// If we see this.state[foo] access, give up.
|
||||
if (node.computed && node.property.type !== 'Literal') {
|
||||
classInfo = null;
|
||||
return;
|
||||
}
|
||||
// Otherwise, record that we saw this property being accessed.
|
||||
addUsedStateField(node.property);
|
||||
// If we see a `this.state` access in a CallExpression, give up.
|
||||
} else if (isStateReference(node) && astUtil.isCallExpression(node.parent)) {
|
||||
classInfo = null;
|
||||
}
|
||||
},
|
||||
|
||||
JSXSpreadAttribute(node) {
|
||||
if (classInfo && isStateReference(node.argument)) {
|
||||
classInfo = null;
|
||||
}
|
||||
},
|
||||
|
||||
'ExperimentalSpreadProperty, SpreadElement'(node) {
|
||||
if (classInfo && isStateReference(node.argument)) {
|
||||
classInfo = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
15
node_modules/eslint-plugin-react/lib/rules/no-will-update-set-state.js
generated
vendored
Normal file
15
node_modules/eslint-plugin-react/lib/rules/no-will-update-set-state.js
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @fileoverview Prevent usage of setState in componentWillUpdate
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const makeNoMethodSetStateRule = require('../util/makeNoMethodSetStateRule');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = makeNoMethodSetStateRule(
|
||||
'componentWillUpdate',
|
||||
(context) => testReactVersion(context, '>= 16.3.0')
|
||||
);
|
||||
58
node_modules/eslint-plugin-react/lib/rules/prefer-es6-class.js
generated
vendored
Normal file
58
node_modules/eslint-plugin-react/lib/rules/prefer-es6-class.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @fileoverview Enforce ES5 or ES6 class for React Components
|
||||
* @author Dan Hamilton
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
shouldUseES6Class: 'Component should use es6 class instead of createClass',
|
||||
shouldUseCreateClass: 'Component should use createClass instead of es6 class',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce ES5 or ES6 class for React Components',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('prefer-es6-class'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
enum: ['always', 'never'],
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || 'always';
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (componentUtil.isES5Component(node, context) && configuration === 'always') {
|
||||
report(context, messages.shouldUseES6Class, 'shouldUseES6Class', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
ClassDeclaration(node) {
|
||||
if (componentUtil.isES6Component(node, context) && configuration === 'never') {
|
||||
report(context, messages.shouldUseCreateClass, 'shouldUseCreateClass', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
162
node_modules/eslint-plugin-react/lib/rules/prefer-exact-props.js
generated
vendored
Normal file
162
node_modules/eslint-plugin-react/lib/rules/prefer-exact-props.js
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @fileoverview Prefer exact proptype definitions
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const propsUtil = require('../util/props');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const variableUtil = require('../util/variable');
|
||||
const report = require('../util/report');
|
||||
const getText = require('../util/eslint').getText;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propTypes: 'Component propTypes should be exact by using {{exactPropWrappers}}.',
|
||||
flow: 'Component flow props should be set with exact objects.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Prefer exact proptype definitions',
|
||||
category: 'Possible Errors',
|
||||
recommended: false,
|
||||
url: docsUrl('prefer-exact-props'),
|
||||
},
|
||||
messages,
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const typeAliases = {};
|
||||
const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context);
|
||||
|
||||
function getPropTypesErrorMessage() {
|
||||
const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers);
|
||||
const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers;
|
||||
return { exactPropWrappers: message };
|
||||
}
|
||||
|
||||
function isNonExactObjectTypeAnnotation(node) {
|
||||
return (
|
||||
node
|
||||
&& node.type === 'ObjectTypeAnnotation'
|
||||
&& node.properties.length > 0
|
||||
&& !node.exact
|
||||
);
|
||||
}
|
||||
|
||||
function hasNonExactObjectTypeAnnotation(node) {
|
||||
const typeAnnotation = node.typeAnnotation;
|
||||
return (
|
||||
typeAnnotation
|
||||
&& typeAnnotation.typeAnnotation
|
||||
&& isNonExactObjectTypeAnnotation(typeAnnotation.typeAnnotation)
|
||||
);
|
||||
}
|
||||
|
||||
function hasGenericTypeAnnotation(node) {
|
||||
const typeAnnotation = node.typeAnnotation;
|
||||
return (
|
||||
typeAnnotation
|
||||
&& typeAnnotation.typeAnnotation
|
||||
&& typeAnnotation.typeAnnotation.type === 'GenericTypeAnnotation'
|
||||
);
|
||||
}
|
||||
|
||||
function isNonEmptyObjectExpression(node) {
|
||||
return (
|
||||
node
|
||||
&& node.type === 'ObjectExpression'
|
||||
&& node.properties.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function isNonExactPropWrapperFunction(node) {
|
||||
return (
|
||||
astUtil.isCallExpression(node)
|
||||
&& !propWrapperUtil.isExactPropWrapperFunction(context, getText(context, node.callee))
|
||||
);
|
||||
}
|
||||
|
||||
function reportPropTypesError(node) {
|
||||
report(context, messages.propTypes, 'propTypes', {
|
||||
node,
|
||||
data: getPropTypesErrorMessage(),
|
||||
});
|
||||
}
|
||||
|
||||
function reportFlowError(node) {
|
||||
report(context, messages.flow, 'flow', {
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
TypeAlias(node) {
|
||||
// working around an issue with eslint@3 and babel-eslint not finding the TypeAlias in scope
|
||||
typeAliases[node.id.name] = node;
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNonExactObjectTypeAnnotation(node)) {
|
||||
reportFlowError(node);
|
||||
} else if (exactWrappers.size > 0 && isNonEmptyObjectExpression(node.value)) {
|
||||
reportPropTypesError(node);
|
||||
} else if (exactWrappers.size > 0 && isNonExactPropWrapperFunction(node.value)) {
|
||||
reportPropTypesError(node);
|
||||
}
|
||||
},
|
||||
|
||||
Identifier(node) {
|
||||
if (!utils.getStatelessComponent(node.parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasNonExactObjectTypeAnnotation(node)) {
|
||||
reportFlowError(node);
|
||||
} else if (hasGenericTypeAnnotation(node)) {
|
||||
const identifier = node.typeAnnotation.typeAnnotation.id.name;
|
||||
const typeAlias = typeAliases[identifier];
|
||||
const propsDefinition = typeAlias ? typeAlias.right : null;
|
||||
if (isNonExactObjectTypeAnnotation(propsDefinition)) {
|
||||
reportFlowError(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const right = node.parent.right;
|
||||
if (isNonEmptyObjectExpression(right)) {
|
||||
reportPropTypesError(node);
|
||||
} else if (isNonExactPropWrapperFunction(right)) {
|
||||
reportPropTypesError(node);
|
||||
} else if (right.type === 'Identifier') {
|
||||
const identifier = right.name;
|
||||
const propsDefinition = variableUtil.findVariableByName(context, node, identifier);
|
||||
if (isNonEmptyObjectExpression(propsDefinition)) {
|
||||
reportPropTypesError(node);
|
||||
} else if (isNonExactPropWrapperFunction(propsDefinition)) {
|
||||
reportPropTypesError(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
117
node_modules/eslint-plugin-react/lib/rules/prefer-read-only-props.js
generated
vendored
Normal file
117
node_modules/eslint-plugin-react/lib/rules/prefer-read-only-props.js
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @fileoverview Require component props to be typed as read-only.
|
||||
* @author Luke Zapart
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const flatMap = require('array.prototype.flatmap');
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
function isFlowPropertyType(node) {
|
||||
return node.type === 'ObjectTypeProperty';
|
||||
}
|
||||
|
||||
function isTypescriptPropertyType(node) {
|
||||
return node.type === 'TSPropertySignature';
|
||||
}
|
||||
|
||||
function isCovariant(node) {
|
||||
return (node.variance && node.variance.kind === 'plus')
|
||||
|| (
|
||||
node.parent
|
||||
&& node.parent.parent
|
||||
&& node.parent.parent.parent
|
||||
&& node.parent.parent.parent.id
|
||||
&& node.parent.parent.parent.id.name === '$ReadOnly'
|
||||
);
|
||||
}
|
||||
|
||||
function isReadonly(node) {
|
||||
return (
|
||||
node.typeAnnotation
|
||||
&& node.typeAnnotation.parent
|
||||
&& node.typeAnnotation.parent.readonly
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
readOnlyProp: 'Prop \'{{name}}\' should be read-only.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce that props are read-only',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('prefer-read-only-props'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
function reportReadOnlyProp(prop, propName, fixer) {
|
||||
report(context, messages.readOnlyProp, 'readOnlyProp', {
|
||||
node: prop.node,
|
||||
data: {
|
||||
name: propName,
|
||||
},
|
||||
fix: fixer,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
flatMap(
|
||||
values(components.list()),
|
||||
(component) => component.declaredPropTypes || []
|
||||
).forEach((declaredPropTypes) => {
|
||||
Object.keys(declaredPropTypes).forEach((propName) => {
|
||||
const prop = declaredPropTypes[propName];
|
||||
if (!prop.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFlowPropertyType(prop.node)) {
|
||||
if (!isCovariant(prop.node)) {
|
||||
reportReadOnlyProp(prop, propName, (fixer) => {
|
||||
if (!prop.node.variance) {
|
||||
// Insert covariance
|
||||
return fixer.insertTextBefore(prop.node, '+');
|
||||
}
|
||||
|
||||
// Replace contravariance with covariance
|
||||
return fixer.replaceText(prop.node.variance, '+');
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTypescriptPropertyType(prop.node)) {
|
||||
if (!isReadonly(prop.node)) {
|
||||
reportReadOnlyProp(prop, propName, (fixer) => (
|
||||
fixer.insertTextBefore(prop.node, 'readonly ')
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
397
node_modules/eslint-plugin-react/lib/rules/prefer-stateless-function.js
generated
vendored
Normal file
397
node_modules/eslint-plugin-react/lib/rules/prefer-stateless-function.js
generated
vendored
Normal file
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* @fileoverview Enforce stateless components to be written as a pure function
|
||||
* @author Yannick Croissant
|
||||
* @author Alberto Rodríguez
|
||||
* @copyright 2015 Alberto Rodríguez. All rights reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const testReactVersion = require('../util/version').testReactVersion;
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getScope = eslintUtil.getScope;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
componentShouldBePure: 'Component should be written as a pure function',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce stateless components to be written as a pure function',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('prefer-stateless-function'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignorePureComponents: {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components, utils) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignorePureComponents = configuration.ignorePureComponents || false;
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a given array of statements is a single call of `super`.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode[]} body - An array of statements to check.
|
||||
* @returns {boolean} `true` if the body is a single call of `super`.
|
||||
*/
|
||||
function isSingleSuperCall(body) {
|
||||
return (
|
||||
body.length === 1
|
||||
&& body[0].type === 'ExpressionStatement'
|
||||
&& astUtil.isCallExpression(body[0].expression)
|
||||
&& body[0].expression.callee.type === 'Super'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given node is a pattern which doesn't have any side effects.
|
||||
* Default parameters and Destructuring parameters can have side effects.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode} node - A pattern node.
|
||||
* @returns {boolean} `true` if the node doesn't have any side effects.
|
||||
*/
|
||||
function isSimple(node) {
|
||||
return node.type === 'Identifier' || node.type === 'RestElement';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given array of expressions is `...arguments` or not.
|
||||
* `super(...arguments)` passes all arguments through.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode[]} superArgs - An array of expressions to check.
|
||||
* @returns {boolean} `true` if the superArgs is `...arguments`.
|
||||
*/
|
||||
function isSpreadArguments(superArgs) {
|
||||
return (
|
||||
superArgs.length === 1
|
||||
&& superArgs[0].type === 'SpreadElement'
|
||||
&& superArgs[0].argument.type === 'Identifier'
|
||||
&& superArgs[0].argument.name === 'arguments'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether given 2 nodes are identifiers which have the same name or not.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode} ctorParam - A node to check.
|
||||
* @param {ASTNode} superArg - A node to check.
|
||||
* @returns {boolean} `true` if the nodes are identifiers which have the same
|
||||
* name.
|
||||
*/
|
||||
function isValidIdentifierPair(ctorParam, superArg) {
|
||||
return (
|
||||
ctorParam.type === 'Identifier'
|
||||
&& superArg.type === 'Identifier'
|
||||
&& ctorParam.name === superArg.name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether given 2 nodes are a rest/spread pair which has the same values.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode} ctorParam - A node to check.
|
||||
* @param {ASTNode} superArg - A node to check.
|
||||
* @returns {boolean} `true` if the nodes are a rest/spread pair which has the
|
||||
* same values.
|
||||
*/
|
||||
function isValidRestSpreadPair(ctorParam, superArg) {
|
||||
return (
|
||||
ctorParam.type === 'RestElement'
|
||||
&& superArg.type === 'SpreadElement'
|
||||
&& isValidIdentifierPair(ctorParam.argument, superArg.argument)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether given 2 nodes have the same value or not.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode} ctorParam - A node to check.
|
||||
* @param {ASTNode} superArg - A node to check.
|
||||
* @returns {boolean} `true` if the nodes have the same value or not.
|
||||
*/
|
||||
function isValidPair(ctorParam, superArg) {
|
||||
return (
|
||||
isValidIdentifierPair(ctorParam, superArg)
|
||||
|| isValidRestSpreadPair(ctorParam, superArg)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the parameters of a constructor and the arguments of `super()`
|
||||
* have the same values or not.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {ASTNode[]} ctorParams - The parameters of a constructor to check.
|
||||
* @param {ASTNode} superArgs - The arguments of `super()` to check.
|
||||
* @returns {boolean} `true` if those have the same values.
|
||||
*/
|
||||
function isPassingThrough(ctorParams, superArgs) {
|
||||
if (ctorParams.length !== superArgs.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < ctorParams.length; ++i) {
|
||||
if (!isValidPair(ctorParams[i], superArgs[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the constructor body is a redundant super call.
|
||||
* @see eslint no-useless-constructor rule
|
||||
* @param {Array} body - constructor body content.
|
||||
* @param {Array} ctorParams - The params to check against super call.
|
||||
* @returns {boolean} true if the constructor body is redundant
|
||||
*/
|
||||
function isRedundantSuperCall(body, ctorParams) {
|
||||
return (
|
||||
isSingleSuperCall(body)
|
||||
&& ctorParams.every(isSimple)
|
||||
&& (
|
||||
isSpreadArguments(body[0].expression.arguments)
|
||||
|| isPassingThrough(ctorParams, body[0].expression.arguments)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given AST node have any other properties the ones available in stateless components
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if the node has at least one other property, false if not.
|
||||
*/
|
||||
function hasOtherProperties(node) {
|
||||
const properties = astUtil.getComponentProperties(node);
|
||||
return properties.some((property) => {
|
||||
const name = astUtil.getPropertyName(property);
|
||||
const isDisplayName = name === 'displayName';
|
||||
const isPropTypes = name === 'propTypes' || ((name === 'props') && property.typeAnnotation);
|
||||
const contextTypes = name === 'contextTypes';
|
||||
const defaultProps = name === 'defaultProps';
|
||||
const isUselessConstructor = property.kind === 'constructor'
|
||||
&& !!property.value.body
|
||||
&& isRedundantSuperCall(property.value.body.body, property.value.params);
|
||||
const isRender = name === 'render';
|
||||
return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark component as pure as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markSCUAsDeclared(node) {
|
||||
components.set(node, {
|
||||
hasSCU: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark childContextTypes as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markChildContextTypesAsDeclared(node) {
|
||||
components.set(node, {
|
||||
hasChildContextTypes: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a setState as used
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markThisAsUsed(node) {
|
||||
components.set(node, {
|
||||
useThis: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a props or context as used
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markPropsOrContextAsUsed(node) {
|
||||
components.set(node, {
|
||||
usePropsOrContext: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a ref as used
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markRefAsUsed(node) {
|
||||
components.set(node, {
|
||||
useRef: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark return as invalid
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markReturnAsInvalid(node) {
|
||||
components.set(node, {
|
||||
invalidReturn: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a ClassDeclaration as having used decorators
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markDecoratorsAsUsed(node) {
|
||||
components.set(node, {
|
||||
useDecorators: true,
|
||||
});
|
||||
}
|
||||
|
||||
function visitClass(node) {
|
||||
if (ignorePureComponents && componentUtil.isPureComponent(node, context)) {
|
||||
markSCUAsDeclared(node);
|
||||
}
|
||||
|
||||
if (node.decorators && node.decorators.length) {
|
||||
markDecoratorsAsUsed(node);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: visitClass,
|
||||
ClassExpression: visitClass,
|
||||
|
||||
// Mark `this` destructuring as a usage of `this`
|
||||
VariableDeclarator(node) {
|
||||
// Ignore destructuring on other than `this`
|
||||
if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
|
||||
return;
|
||||
}
|
||||
// Ignore `props` and `context`
|
||||
const useThis = node.id.properties.some((property) => {
|
||||
const name = astUtil.getPropertyName(property);
|
||||
return name !== 'props' && name !== 'context';
|
||||
});
|
||||
if (!useThis) {
|
||||
markPropsOrContextAsUsed(node);
|
||||
return;
|
||||
}
|
||||
markThisAsUsed(node);
|
||||
},
|
||||
|
||||
// Mark `this` usage
|
||||
MemberExpression(node) {
|
||||
if (node.object.type !== 'ThisExpression') {
|
||||
if (node.property && node.property.name === 'childContextTypes') {
|
||||
const component = utils.getRelatedComponent(node);
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
markChildContextTypesAsDeclared(component.node);
|
||||
}
|
||||
return;
|
||||
// Ignore calls to `this.props` and `this.context`
|
||||
}
|
||||
if (
|
||||
(node.property.name || node.property.value) === 'props'
|
||||
|| (node.property.name || node.property.value) === 'context'
|
||||
) {
|
||||
markPropsOrContextAsUsed(node);
|
||||
return;
|
||||
}
|
||||
markThisAsUsed(node);
|
||||
},
|
||||
|
||||
// Mark `ref` usage
|
||||
JSXAttribute(node) {
|
||||
const name = getText(context, node.name);
|
||||
if (name !== 'ref') {
|
||||
return;
|
||||
}
|
||||
markRefAsUsed(node);
|
||||
},
|
||||
|
||||
// Mark `render` that do not return some JSX
|
||||
ReturnStatement(node) {
|
||||
let blockNode;
|
||||
let scope = getScope(context, node);
|
||||
while (scope) {
|
||||
blockNode = scope.block && scope.block.parent;
|
||||
if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
|
||||
break;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
const isRender = blockNode
|
||||
&& blockNode.key
|
||||
&& blockNode.key.name === 'render';
|
||||
const allowNull = testReactVersion(context, '>= 15.0.0'); // Stateless components can return null since React 15
|
||||
const isReturningJSX = utils.isReturningJSX(node, !allowNull);
|
||||
const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false);
|
||||
if (
|
||||
!isRender
|
||||
|| (allowNull && (isReturningJSX || isReturningNull))
|
||||
|| (!allowNull && isReturningJSX)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
markReturnAsInvalid(node);
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
values(list)
|
||||
.filter((component) => (
|
||||
!hasOtherProperties(component.node)
|
||||
&& !component.useThis
|
||||
&& !component.useRef
|
||||
&& !component.invalidReturn
|
||||
&& !component.hasChildContextTypes
|
||||
&& !component.useDecorators
|
||||
&& !component.hasSCU
|
||||
&& (
|
||||
componentUtil.isES5Component(component.node, context)
|
||||
|| componentUtil.isES6Component(component.node, context)
|
||||
)
|
||||
))
|
||||
.forEach((component) => {
|
||||
report(context, messages.componentShouldBePure, 'componentShouldBePure', {
|
||||
node: component.node,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
225
node_modules/eslint-plugin-react/lib/rules/prop-types.js
generated
vendored
Normal file
225
node_modules/eslint-plugin-react/lib/rules/prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @fileoverview Prevent missing props validation in a React component definition
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// As for exceptions for props.children or props.className (and alike) look at
|
||||
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
missingPropType: '\'{{name}}\' is missing in props validation',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing props validation in a React component definition',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
url: docsUrl('prop-types'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
customValidators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
skipUndeclared: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignored = configuration.ignore || [];
|
||||
const skipUndeclared = configuration.skipUndeclared || false;
|
||||
|
||||
/**
|
||||
* Checks if the prop is ignored
|
||||
* @param {string} name Name of the prop to check.
|
||||
* @returns {boolean} True if the prop is ignored, false if not.
|
||||
*/
|
||||
function isIgnored(name) {
|
||||
return ignored.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the component must be validated
|
||||
* @param {Object} component The component to process
|
||||
* @returns {boolean} True if the component must be validated, false if not.
|
||||
*/
|
||||
function mustBeValidated(component) {
|
||||
const isSkippedByConfig = skipUndeclared && typeof component.declaredPropTypes === 'undefined';
|
||||
return !!(
|
||||
component
|
||||
&& component.usedPropTypes
|
||||
&& !component.ignorePropsValidation
|
||||
&& !isSkippedByConfig
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Checks if the prop is declared
|
||||
* @param {Object} declaredPropTypes Description of propTypes declared in the current component
|
||||
* @param {string[]} keyList Dot separated name of the prop to check.
|
||||
* @returns {boolean} True if the prop is declared, false if not.
|
||||
*/
|
||||
function internalIsDeclaredInComponent(declaredPropTypes, keyList) {
|
||||
for (let i = 0, j = keyList.length; i < j; i++) {
|
||||
const key = keyList[i];
|
||||
const propType = (
|
||||
declaredPropTypes && (
|
||||
// Check if this key is declared
|
||||
(declaredPropTypes[key] // If not, check if this type accepts any key
|
||||
|| declaredPropTypes.__ANY_KEY__) // eslint-disable-line no-underscore-dangle
|
||||
)
|
||||
);
|
||||
|
||||
if (!propType) {
|
||||
// If it's a computed property, we can't make any further analysis, but is valid
|
||||
return key === '__COMPUTED_PROP__';
|
||||
}
|
||||
if (typeof propType === 'object' && !propType.type) {
|
||||
return true;
|
||||
}
|
||||
// Consider every children as declared
|
||||
if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) {
|
||||
return true;
|
||||
}
|
||||
if (propType.acceptedProperties) {
|
||||
return key in propType.acceptedProperties;
|
||||
}
|
||||
if (propType.type === 'union') {
|
||||
// If we fall in this case, we know there is at least one complex type in the union
|
||||
if (i + 1 >= j) {
|
||||
// this is the last key, accept everything
|
||||
return true;
|
||||
}
|
||||
// non trivial, check all of them
|
||||
const unionTypes = propType.children;
|
||||
const unionPropType = {};
|
||||
for (let k = 0, z = unionTypes.length; k < z; k++) {
|
||||
unionPropType[key] = unionTypes[k];
|
||||
const isValid = internalIsDeclaredInComponent(
|
||||
unionPropType,
|
||||
keyList.slice(i)
|
||||
);
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// every possible union were invalid
|
||||
return false;
|
||||
}
|
||||
declaredPropTypes = propType.children;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prop is declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @param {string[]} names List of names of the prop to check.
|
||||
* @returns {boolean} True if the prop is declared, false if not.
|
||||
*/
|
||||
function isDeclaredInComponent(node, names) {
|
||||
while (node) {
|
||||
const component = components.get(node);
|
||||
|
||||
const isDeclared = component && component.confidence >= 2
|
||||
&& internalIsDeclaredInComponent(component.declaredPropTypes || {}, names);
|
||||
|
||||
if (isDeclared) {
|
||||
return true;
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports undeclared proptypes for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportUndeclaredPropTypes(component) {
|
||||
const undeclareds = component.usedPropTypes.filter((propType) => (
|
||||
propType.node
|
||||
&& !isIgnored(propType.allNames[0])
|
||||
&& !isDeclaredInComponent(component.node, propType.allNames)
|
||||
));
|
||||
undeclareds.forEach((propType) => {
|
||||
report(context, messages.missingPropType, 'missingPropType', {
|
||||
node: propType.node,
|
||||
data: {
|
||||
name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]'),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} component The current component to process
|
||||
* @param {Array} list The all components to process
|
||||
* @returns {boolean} True if the component is nested False if not.
|
||||
*/
|
||||
function checkNestedComponent(component, list) {
|
||||
const componentIsMemo = component.node.callee && component.node.callee.name === 'memo';
|
||||
const argumentIsForwardRef = component.node.arguments && component.node.arguments[0].callee && component.node.arguments[0].callee.name === 'forwardRef';
|
||||
if (componentIsMemo && argumentIsForwardRef) {
|
||||
const forwardComponent = list.find(
|
||||
(innerComponent) => (
|
||||
innerComponent.node.range[0] === component.node.arguments[0].range[0]
|
||||
&& innerComponent.node.range[0] === component.node.arguments[0].range[0]
|
||||
));
|
||||
|
||||
const isValidated = mustBeValidated(forwardComponent);
|
||||
const isIgnorePropsValidation = forwardComponent.ignorePropsValidation;
|
||||
|
||||
return isIgnorePropsValidation || isValidated;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
// Report undeclared proptypes for all classes
|
||||
values(list)
|
||||
.filter((component) => mustBeValidated(component))
|
||||
.forEach((component) => {
|
||||
if (checkNestedComponent(component, values(list))) return;
|
||||
reportUndeclaredPropTypes(component);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
56
node_modules/eslint-plugin-react/lib/rules/react-in-jsx-scope.js
generated
vendored
Normal file
56
node_modules/eslint-plugin-react/lib/rules/react-in-jsx-scope.js
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @fileoverview Prevent missing React when using JSX
|
||||
* @author Glen Mailer
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const pragmaUtil = require('../util/pragma');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
notInScope: '\'{{name}}\' must be in scope when using JSX',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow missing React when using JSX',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('react-in-jsx-scope'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const pragma = pragmaUtil.getFromContext(context);
|
||||
|
||||
function checkIfReactIsInScope(node) {
|
||||
if (variableUtil.getVariableFromContext(context, node, pragma)) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.notInScope, 'notInScope', {
|
||||
node,
|
||||
data: {
|
||||
name: pragma,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement: checkIfReactIsInScope,
|
||||
JSXOpeningFragment: checkIfReactIsInScope,
|
||||
};
|
||||
},
|
||||
};
|
||||
209
node_modules/eslint-plugin-react/lib/rules/require-default-props.js
generated
vendored
Normal file
209
node_modules/eslint-plugin-react/lib/rules/require-default-props.js
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @fileOverview Enforce a defaultProps definition for every prop that is not a required prop.
|
||||
* @author Vitor Balocco
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const entries = require('object.entries');
|
||||
const values = require('object.values');
|
||||
const Components = require('../util/Components');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const astUtil = require('../util/ast');
|
||||
const report = require('../util/report');
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noDefaultWithRequired: 'propType "{{name}}" is required and should not have a defaultProps declaration.',
|
||||
shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.',
|
||||
noDefaultPropsWithFunction: 'Don’t use defaultProps with function components.',
|
||||
shouldAssignObjectDefault: 'propType "{{name}}" is not required, but has no corresponding default argument value.',
|
||||
destructureInSignature: 'Must destructure props in the function signature to initialize an optional prop.',
|
||||
};
|
||||
|
||||
function isPropWithNoDefaulVal(prop) {
|
||||
if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
|
||||
return false;
|
||||
}
|
||||
return prop.value.type !== 'AssignmentPattern';
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce a defaultProps definition for every prop that is not a required prop',
|
||||
category: 'Best Practices',
|
||||
url: docsUrl('require-default-props'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
forbidDefaultForRequired: {
|
||||
type: 'boolean',
|
||||
},
|
||||
classes: {
|
||||
enum: ['defaultProps', 'ignore'],
|
||||
},
|
||||
functions: {
|
||||
enum: ['defaultArguments', 'defaultProps', 'ignore'],
|
||||
},
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
ignoreFunctionalComponents: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false;
|
||||
const classes = configuration.classes || 'defaultProps';
|
||||
/**
|
||||
* @todo
|
||||
* - Remove ignoreFunctionalComponents
|
||||
* - Change default to 'defaultArguments'
|
||||
*/
|
||||
const functions = configuration.ignoreFunctionalComponents
|
||||
? 'ignore'
|
||||
: configuration.functions || 'defaultProps';
|
||||
|
||||
/**
|
||||
* Reports all propTypes passed in that don't have a defaultProps counterpart.
|
||||
* @param {Object[]} propTypes List of propTypes to check.
|
||||
* @param {Object} defaultProps Object of defaultProps to check. Keys are the props names.
|
||||
* @return {void}
|
||||
*/
|
||||
function reportPropTypesWithoutDefault(propTypes, defaultProps) {
|
||||
entries(propTypes).forEach((propType) => {
|
||||
const propName = propType[0];
|
||||
const prop = propType[1];
|
||||
|
||||
if (!prop.node) {
|
||||
return;
|
||||
}
|
||||
if (prop.isRequired) {
|
||||
if (forbidDefaultForRequired && defaultProps[propName]) {
|
||||
report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
|
||||
node: prop.node,
|
||||
data: { name: propName },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultProps[propName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(context, messages.shouldHaveDefault, 'shouldHaveDefault', {
|
||||
node: prop.node,
|
||||
data: { name: propName },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If functions option is 'defaultArguments', reports defaultProps is used and all params that doesn't initialized.
|
||||
* @param {Object} componentNode Node of component.
|
||||
* @param {Object[]} declaredPropTypes List of propTypes to check `isRequired`.
|
||||
* @param {Object} defaultProps Object of defaultProps to check used.
|
||||
*/
|
||||
function reportFunctionComponent(componentNode, declaredPropTypes, defaultProps) {
|
||||
if (defaultProps) {
|
||||
report(context, messages.noDefaultPropsWithFunction, 'noDefaultPropsWithFunction', {
|
||||
node: componentNode,
|
||||
});
|
||||
}
|
||||
|
||||
const props = componentNode.params[0];
|
||||
const propTypes = declaredPropTypes;
|
||||
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.type === 'Identifier') {
|
||||
const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired);
|
||||
if (hasOptionalProp) {
|
||||
report(context, messages.destructureInSignature, 'destructureInSignature', {
|
||||
node: props,
|
||||
});
|
||||
}
|
||||
} else if (props.type === 'ObjectPattern') {
|
||||
// Filter required props with default value and report error
|
||||
props.properties.filter((prop) => {
|
||||
const propName = prop && prop.key && prop.key.name;
|
||||
const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
|
||||
return propTypes[propName] && isPropRequired && !isPropWithNoDefaulVal(prop);
|
||||
}).forEach((prop) => {
|
||||
report(context, messages.noDefaultWithRequired, 'noDefaultWithRequired', {
|
||||
node: prop,
|
||||
data: { name: prop.key.name },
|
||||
});
|
||||
});
|
||||
|
||||
// Filter non required props with no default value and report error
|
||||
props.properties.filter((prop) => {
|
||||
const propName = prop && prop.key && prop.key.name;
|
||||
const isPropRequired = propTypes[propName] && propTypes[propName].isRequired;
|
||||
return propTypes[propName] && !isPropRequired && isPropWithNoDefaulVal(prop);
|
||||
}).forEach((prop) => {
|
||||
report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', {
|
||||
node: prop,
|
||||
data: { name: prop.key.name },
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
const list = components.list();
|
||||
|
||||
values(list).filter((component) => {
|
||||
if (functions === 'ignore' && astUtil.isFunctionLike(component.node)) {
|
||||
return false;
|
||||
}
|
||||
if (classes === 'ignore' && astUtil.isClass(component.node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this defaultProps is "unresolved", then we should ignore this component and not report
|
||||
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators.
|
||||
if (component.defaultProps === 'unresolved') {
|
||||
return false;
|
||||
}
|
||||
return component.declaredPropTypes !== undefined;
|
||||
}).forEach((component) => {
|
||||
if (functions === 'defaultArguments' && astUtil.isFunctionLike(component.node)) {
|
||||
reportFunctionComponent(
|
||||
component.node,
|
||||
component.declaredPropTypes,
|
||||
component.defaultProps
|
||||
);
|
||||
} else {
|
||||
reportPropTypesWithoutDefault(
|
||||
component.declaredPropTypes,
|
||||
component.defaultProps || {}
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
240
node_modules/eslint-plugin-react/lib/rules/require-optimization.js
generated
vendored
Normal file
240
node_modules/eslint-plugin-react/lib/rules/require-optimization.js
generated
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @fileoverview Enforce React components to have a shouldComponentUpdate method
|
||||
* @author Evgueni Naverniouk
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getScope = require('../util/eslint').getScope;
|
||||
|
||||
const messages = {
|
||||
noShouldComponentUpdate: 'Component is not optimized. Please add a shouldComponentUpdate method.',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce React components to have a shouldComponentUpdate method',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
url: docsUrl('require-optimization'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
allowDecorators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
const configuration = context.options[0] || {};
|
||||
const allowDecorators = configuration.allowDecorators || [];
|
||||
|
||||
/**
|
||||
* Checks to see if our component is decorated by PureRenderMixin via reactMixin
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if node is decorated with a PureRenderMixin, false if not.
|
||||
*/
|
||||
function hasPureRenderDecorator(node) {
|
||||
if (node.decorators && node.decorators.length) {
|
||||
for (let i = 0, l = node.decorators.length; i < l; i++) {
|
||||
if (
|
||||
node.decorators[i].expression
|
||||
&& node.decorators[i].expression.callee
|
||||
&& node.decorators[i].expression.callee.object
|
||||
&& node.decorators[i].expression.callee.object.name === 'reactMixin'
|
||||
&& node.decorators[i].expression.callee.property
|
||||
&& node.decorators[i].expression.callee.property.name === 'decorate'
|
||||
&& node.decorators[i].expression.arguments
|
||||
&& node.decorators[i].expression.arguments.length
|
||||
&& node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if our component is custom decorated
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if node is decorated name with a custom decorated, false if not.
|
||||
*/
|
||||
function hasCustomDecorator(node) {
|
||||
const allowLength = allowDecorators.length;
|
||||
|
||||
if (allowLength && node.decorators && node.decorators.length) {
|
||||
for (let i = 0; i < allowLength; i++) {
|
||||
for (let j = 0, l = node.decorators.length; j < l; j++) {
|
||||
const expression = node.decorators[j].expression;
|
||||
if (
|
||||
expression
|
||||
&& expression.name === allowDecorators[i]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a shouldComponentUpdate method
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if we are declaring a shouldComponentUpdate method, false if not.
|
||||
*/
|
||||
function isSCUDeclared(node) {
|
||||
return !!node && node.name === 'shouldComponentUpdate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are declaring a PureRenderMixin mixin
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
* @returns {boolean} True if we are declaring a PureRenderMixin method, false if not.
|
||||
*/
|
||||
function isPureRenderDeclared(node) {
|
||||
let hasPR = false;
|
||||
if (node.value && node.value.elements) {
|
||||
for (let i = 0, l = node.value.elements.length; i < l; i++) {
|
||||
if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') {
|
||||
hasPR = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
!!node
|
||||
&& node.key.name === 'mixins'
|
||||
&& hasPR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark shouldComponentUpdate as declared
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markSCUAsDeclared(node) {
|
||||
components.set(node, {
|
||||
hasSCU: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports missing optimization for a given component
|
||||
* @param {Object} component The component to process
|
||||
*/
|
||||
function reportMissingOptimization(component) {
|
||||
report(context, messages.noShouldComponentUpdate, 'noShouldComponentUpdate', {
|
||||
node: component.node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are declaring function in class
|
||||
* @param {ASTNode} node
|
||||
* @returns {boolean} True if we are declaring function in class, false if not.
|
||||
*/
|
||||
function isFunctionInClass(node) {
|
||||
let blockNode;
|
||||
let scope = getScope(context, node);
|
||||
while (scope) {
|
||||
blockNode = scope.block;
|
||||
if (blockNode && blockNode.type === 'ClassDeclaration') {
|
||||
return true;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression(node) {
|
||||
// Skip if the function is declared in the class
|
||||
if (isFunctionInClass(node)) {
|
||||
return;
|
||||
}
|
||||
// Stateless Functional Components cannot be optimized (yet)
|
||||
markSCUAsDeclared(node);
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
if (!(
|
||||
hasPureRenderDecorator(node)
|
||||
|| hasCustomDecorator(node)
|
||||
|| componentUtil.isPureComponent(node, context)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
markSCUAsDeclared(node);
|
||||
},
|
||||
|
||||
FunctionDeclaration(node) {
|
||||
// Skip if the function is declared in the class
|
||||
if (isFunctionInClass(node)) {
|
||||
return;
|
||||
}
|
||||
// Stateless Functional Components cannot be optimized (yet)
|
||||
markSCUAsDeclared(node);
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
// Skip if the function is declared in the class
|
||||
if (isFunctionInClass(node)) {
|
||||
return;
|
||||
}
|
||||
// Stateless Functional Components cannot be optimized (yet)
|
||||
markSCUAsDeclared(node);
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (!isSCUDeclared(node.key)) {
|
||||
return;
|
||||
}
|
||||
markSCUAsDeclared(node);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
// Search for the shouldComponentUpdate declaration
|
||||
const found = node.properties.some((property) => (
|
||||
property.key
|
||||
&& (isSCUDeclared(property.key) || isPureRenderDeclared(property))
|
||||
));
|
||||
if (found) {
|
||||
markSCUAsDeclared(node);
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
// Report missing shouldComponentUpdate for all components
|
||||
values(components.list())
|
||||
.filter((component) => !component.hasSCU)
|
||||
.forEach((component) => {
|
||||
reportMissingOptimization(component);
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
106
node_modules/eslint-plugin-react/lib/rules/require-render-return.js
generated
vendored
Normal file
106
node_modules/eslint-plugin-react/lib/rules/require-render-return.js
generated
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @fileoverview Enforce ES5 or ES6 class for returning value in render function.
|
||||
* @author Mark Orel
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const values = require('object.values');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const astUtil = require('../util/ast');
|
||||
const componentUtil = require('../util/componentUtil');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const getAncestors = require('../util/eslint').getAncestors;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
noRenderReturn: 'Your render method should have a return statement',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce ES5 or ES6 class for returning value in render function',
|
||||
category: 'Possible Errors',
|
||||
recommended: true,
|
||||
url: docsUrl('require-render-return'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
/**
|
||||
* Mark a return statement as present
|
||||
* @param {ASTNode} node The AST node being checked.
|
||||
*/
|
||||
function markReturnStatementPresent(node) {
|
||||
components.set(node, {
|
||||
hasReturnStatement: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find render method in a given AST node
|
||||
* @param {ASTNode} node The component to find render method.
|
||||
* @returns {ASTNode} Method node if found, undefined if not.
|
||||
*/
|
||||
function findRenderMethod(node) {
|
||||
const properties = astUtil.getComponentProperties(node);
|
||||
return properties
|
||||
.filter((property) => astUtil.getPropertyName(property) === 'render' && property.value)
|
||||
.find((property) => astUtil.isFunctionLikeExpression(property.value));
|
||||
}
|
||||
|
||||
return {
|
||||
ReturnStatement(node) {
|
||||
const ancestors = getAncestors(context, node).reverse();
|
||||
let depth = 0;
|
||||
ancestors.forEach((ancestor) => {
|
||||
if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
|
||||
depth += 1;
|
||||
}
|
||||
if (
|
||||
/(MethodDefinition|Property|ClassProperty|PropertyDefinition)$/.test(ancestor.type)
|
||||
&& astUtil.getPropertyName(ancestor) === 'render'
|
||||
&& depth <= 1
|
||||
) {
|
||||
markReturnStatementPresent(node);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ArrowFunctionExpression(node) {
|
||||
if (node.expression === false || astUtil.getPropertyName(node.parent) !== 'render') {
|
||||
return;
|
||||
}
|
||||
markReturnStatementPresent(node);
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
values(components.list())
|
||||
.filter((component) => (
|
||||
findRenderMethod(component.node)
|
||||
&& !component.hasReturnStatement
|
||||
&& (
|
||||
componentUtil.isES5Component(component.node, context)
|
||||
|| componentUtil.isES6Component(component.node, context)
|
||||
)
|
||||
))
|
||||
.forEach((component) => {
|
||||
report(context, messages.noRenderReturn, 'noRenderReturn', {
|
||||
node: findRenderMethod(component.node),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
104
node_modules/eslint-plugin-react/lib/rules/self-closing-comp.js
generated
vendored
Normal file
104
node_modules/eslint-plugin-react/lib/rules/self-closing-comp.js
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @fileoverview Prevent extra closing tags for components without children
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const jsxUtil = require('../util/jsx');
|
||||
const report = require('../util/report');
|
||||
|
||||
const optionDefaults = { component: true, html: true };
|
||||
|
||||
function isComponent(node) {
|
||||
return (
|
||||
node.name
|
||||
&& (node.name.type === 'JSXIdentifier' || node.name.type === 'JSXMemberExpression')
|
||||
&& !jsxUtil.isDOMComponent(node)
|
||||
);
|
||||
}
|
||||
|
||||
function childrenIsEmpty(node) {
|
||||
return node.parent.children.length === 0;
|
||||
}
|
||||
|
||||
function childrenIsMultilineSpaces(node) {
|
||||
const childrens = node.parent.children;
|
||||
|
||||
return (
|
||||
childrens.length === 1
|
||||
&& (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText')
|
||||
&& childrens[0].value.indexOf('\n') !== -1
|
||||
&& childrens[0].value.replace(/(?!\xA0)\s/g, '') === ''
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
notSelfClosing: 'Empty components are self-closing',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow extra closing tags for components without children',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('self-closing-comp'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
component: {
|
||||
default: optionDefaults.component,
|
||||
type: 'boolean',
|
||||
},
|
||||
html: {
|
||||
default: optionDefaults.html,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
function isShouldBeSelfClosed(node) {
|
||||
const configuration = Object.assign({}, optionDefaults, context.options[0]);
|
||||
return (
|
||||
(configuration.component && isComponent(node))
|
||||
|| (configuration.html && jsxUtil.isDOMComponent(node))
|
||||
) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node));
|
||||
}
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (!isShouldBeSelfClosed(node)) {
|
||||
return;
|
||||
}
|
||||
report(context, messages.notSelfClosing, 'notSelfClosing', {
|
||||
node,
|
||||
fix(fixer) {
|
||||
// Represents the last character of the JSXOpeningElement, the '>' character
|
||||
const openingElementEnding = node.range[1] - 1;
|
||||
// Represents the last character of the JSXClosingElement, the '>' character
|
||||
const closingElementEnding = node.parent.closingElement.range[1];
|
||||
|
||||
// Replace />.*<\/.*>/ with '/>'
|
||||
const range = [openingElementEnding, closingElementEnding];
|
||||
return fixer.replaceTextRange(range, ' />');
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
449
node_modules/eslint-plugin-react/lib/rules/sort-comp.js
generated
vendored
Normal file
449
node_modules/eslint-plugin-react/lib/rules/sort-comp.js
generated
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* @fileoverview Enforce component methods order
|
||||
* @author Yannick Croissant
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const has = require('hasown');
|
||||
const entries = require('object.entries');
|
||||
const values = require('object.values');
|
||||
const arrayIncludes = require('array-includes');
|
||||
|
||||
const Components = require('../util/Components');
|
||||
const astUtil = require('../util/ast');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
|
||||
const defaultConfig = {
|
||||
order: [
|
||||
'static-methods',
|
||||
'lifecycle',
|
||||
'everything-else',
|
||||
'render',
|
||||
],
|
||||
groups: {
|
||||
lifecycle: [
|
||||
'displayName',
|
||||
'propTypes',
|
||||
'contextTypes',
|
||||
'childContextTypes',
|
||||
'mixins',
|
||||
'statics',
|
||||
'defaultProps',
|
||||
'constructor',
|
||||
'getDefaultProps',
|
||||
'state',
|
||||
'getInitialState',
|
||||
'getChildContext',
|
||||
'getDerivedStateFromProps',
|
||||
'componentWillMount',
|
||||
'UNSAFE_componentWillMount',
|
||||
'componentDidMount',
|
||||
'componentWillReceiveProps',
|
||||
'UNSAFE_componentWillReceiveProps',
|
||||
'shouldComponentUpdate',
|
||||
'componentWillUpdate',
|
||||
'UNSAFE_componentWillUpdate',
|
||||
'getSnapshotBeforeUpdate',
|
||||
'componentDidUpdate',
|
||||
'componentDidCatch',
|
||||
'componentWillUnmount',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the methods order from the default config and the user config
|
||||
* @param {Object} userConfig The user configuration.
|
||||
* @returns {Array} Methods order
|
||||
*/
|
||||
function getMethodsOrder(userConfig) {
|
||||
userConfig = userConfig || {};
|
||||
|
||||
const groups = Object.assign({}, defaultConfig.groups, userConfig.groups);
|
||||
const order = userConfig.order || defaultConfig.order;
|
||||
|
||||
let config = [];
|
||||
let entry;
|
||||
for (let i = 0, j = order.length; i < j; i++) {
|
||||
entry = order[i];
|
||||
if (has(groups, entry)) {
|
||||
config = config.concat(groups[entry]);
|
||||
} else {
|
||||
config.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
unsortedProps: '{{propA}} should be placed {{position}} {{propB}}',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce component methods order',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('sort-comp'),
|
||||
},
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
order: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'^.*$': {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create: Components.detect((context, components) => {
|
||||
/** @satisfies {Record<string, { node: ASTNode, score: number, closest: { distance: number, ref: { node: null | ASTNode, index: number } } }>} */
|
||||
const errors = {};
|
||||
const methodsOrder = getMethodsOrder(context.options[0]);
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
const regExpRegExp = /\/(.*)\/([gimsuy]*)/;
|
||||
|
||||
/**
|
||||
* Get indexes of the matching patterns in methods order configuration
|
||||
* @param {Object} method - Method metadata.
|
||||
* @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match.
|
||||
*/
|
||||
function getRefPropIndexes(method) {
|
||||
const methodGroupIndexes = [];
|
||||
|
||||
methodsOrder.forEach((currentGroup, groupIndex) => {
|
||||
if (currentGroup === 'getters') {
|
||||
if (method.getter) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'setters') {
|
||||
if (method.setter) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'type-annotations') {
|
||||
if (method.typeAnnotation) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'static-variables') {
|
||||
if (method.staticVariable) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'static-methods') {
|
||||
if (method.staticMethod) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'instance-variables') {
|
||||
if (method.instanceVariable) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === 'instance-methods') {
|
||||
if (method.instanceMethod) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (arrayIncludes([
|
||||
'displayName',
|
||||
'propTypes',
|
||||
'contextTypes',
|
||||
'childContextTypes',
|
||||
'mixins',
|
||||
'statics',
|
||||
'defaultProps',
|
||||
'constructor',
|
||||
'getDefaultProps',
|
||||
'state',
|
||||
'getInitialState',
|
||||
'getChildContext',
|
||||
'getDerivedStateFromProps',
|
||||
'componentWillMount',
|
||||
'UNSAFE_componentWillMount',
|
||||
'componentDidMount',
|
||||
'componentWillReceiveProps',
|
||||
'UNSAFE_componentWillReceiveProps',
|
||||
'shouldComponentUpdate',
|
||||
'componentWillUpdate',
|
||||
'UNSAFE_componentWillUpdate',
|
||||
'getSnapshotBeforeUpdate',
|
||||
'componentDidUpdate',
|
||||
'componentDidCatch',
|
||||
'componentWillUnmount',
|
||||
'render',
|
||||
], currentGroup)) {
|
||||
if (currentGroup === method.name) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else {
|
||||
// Is the group a regex?
|
||||
const isRegExp = currentGroup.match(regExpRegExp);
|
||||
if (isRegExp) {
|
||||
const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name);
|
||||
if (isMatching) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
} else if (currentGroup === method.name) {
|
||||
methodGroupIndexes.push(groupIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No matching pattern, return 'everything-else' index
|
||||
if (methodGroupIndexes.length === 0) {
|
||||
const everythingElseIndex = methodsOrder.indexOf('everything-else');
|
||||
|
||||
if (everythingElseIndex !== -1) {
|
||||
methodGroupIndexes.push(everythingElseIndex);
|
||||
} else {
|
||||
// No matching pattern and no 'everything-else' group
|
||||
methodGroupIndexes.push(Infinity);
|
||||
}
|
||||
}
|
||||
|
||||
return methodGroupIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties name
|
||||
* @param {Object} node - Property.
|
||||
* @returns {string} Property name.
|
||||
*/
|
||||
function getPropertyName(node) {
|
||||
if (node.kind === 'get') {
|
||||
return 'getter functions';
|
||||
}
|
||||
|
||||
if (node.kind === 'set') {
|
||||
return 'setter functions';
|
||||
}
|
||||
|
||||
return astUtil.getPropertyName(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new error in the error list
|
||||
* @param {Object} propA - Mispositioned property.
|
||||
* @param {Object} propB - Reference property.
|
||||
*/
|
||||
function storeError(propA, propB) {
|
||||
// Initialize the error object if needed
|
||||
if (!errors[propA.index]) {
|
||||
errors[propA.index] = {
|
||||
node: propA.node,
|
||||
score: 0,
|
||||
closest: {
|
||||
distance: Infinity,
|
||||
ref: {
|
||||
node: null,
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
// Increment the prop score
|
||||
errors[propA.index].score += 1;
|
||||
// Stop here if we already have pushed another node at this position
|
||||
if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) {
|
||||
return;
|
||||
}
|
||||
// Stop here if we already have a closer reference
|
||||
if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) {
|
||||
return;
|
||||
}
|
||||
// Update the closest reference
|
||||
errors[propA.index].closest.distance = Math.abs(propA.index - propB.index);
|
||||
errors[propA.index].closest.ref.node = propB.node;
|
||||
errors[propA.index].closest.ref.index = propB.index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedupe errors, only keep the ones with the highest score and delete the others
|
||||
*/
|
||||
function dedupeErrors() {
|
||||
entries(errors).forEach((entry) => {
|
||||
const i = entry[0];
|
||||
const error = entry[1];
|
||||
|
||||
const index = error.closest.ref.index;
|
||||
if (errors[index]) {
|
||||
if (error.score > errors[index].score) {
|
||||
delete errors[index];
|
||||
} else {
|
||||
delete errors[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report errors
|
||||
*/
|
||||
function reportErrors() {
|
||||
dedupeErrors();
|
||||
|
||||
entries(errors).forEach((entry) => {
|
||||
const nodeA = entry[1].node;
|
||||
const nodeB = entry[1].closest.ref.node;
|
||||
const indexA = entry[0];
|
||||
const indexB = entry[1].closest.ref.index;
|
||||
|
||||
report(context, messages.unsortedProps, 'unsortedProps', {
|
||||
node: nodeA,
|
||||
data: {
|
||||
propA: getPropertyName(nodeA),
|
||||
propB: getPropertyName(nodeB),
|
||||
position: indexA < indexB ? 'before' : 'after',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two properties and find out if they are in the right order
|
||||
* @param {Array} propertiesInfos Array containing all the properties metadata.
|
||||
* @param {Object} propA First property name and metadata
|
||||
* @param {Object} propB Second property name.
|
||||
* @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties.
|
||||
*/
|
||||
function comparePropsOrder(propertiesInfos, propA, propB) {
|
||||
let i;
|
||||
let j;
|
||||
let k;
|
||||
let l;
|
||||
let refIndexA;
|
||||
let refIndexB;
|
||||
|
||||
// Get references indexes (the correct position) for given properties
|
||||
const refIndexesA = getRefPropIndexes(propA);
|
||||
const refIndexesB = getRefPropIndexes(propB);
|
||||
|
||||
// Get current indexes for given properties
|
||||
const classIndexA = propertiesInfos.indexOf(propA);
|
||||
const classIndexB = propertiesInfos.indexOf(propB);
|
||||
|
||||
// Loop around the references indexes for the 1st property
|
||||
for (i = 0, j = refIndexesA.length; i < j; i++) {
|
||||
refIndexA = refIndexesA[i];
|
||||
|
||||
// Loop around the properties for the 2nd property (for comparison)
|
||||
for (k = 0, l = refIndexesB.length; k < l; k++) {
|
||||
refIndexB = refIndexesB[k];
|
||||
|
||||
if (
|
||||
// Comparing the same properties
|
||||
refIndexA === refIndexB
|
||||
// 1st property is placed before the 2nd one in reference and in current component
|
||||
|| ((refIndexA < refIndexB) && (classIndexA < classIndexB))
|
||||
// 1st property is placed after the 2nd one in reference and in current component
|
||||
|| ((refIndexA > refIndexB) && (classIndexA > classIndexB))
|
||||
) {
|
||||
return {
|
||||
correct: true,
|
||||
indexA: classIndexA,
|
||||
indexB: classIndexB,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We did not find any correct match between reference and current component
|
||||
return {
|
||||
correct: false,
|
||||
indexA: refIndexA,
|
||||
indexB: refIndexB,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check properties order from a properties list and store the eventual errors
|
||||
* @param {Array} properties Array containing all the properties.
|
||||
*/
|
||||
function checkPropsOrder(properties) {
|
||||
const propertiesInfos = properties.map((node) => ({
|
||||
name: getPropertyName(node),
|
||||
getter: node.kind === 'get',
|
||||
setter: node.kind === 'set',
|
||||
staticVariable: node.static
|
||||
&& (node.type === 'ClassProperty' || node.type === 'PropertyDefinition')
|
||||
&& (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
|
||||
staticMethod: node.static
|
||||
&& (node.type === 'ClassProperty' || node.type === 'PropertyDefinition' || node.type === 'MethodDefinition')
|
||||
&& node.value
|
||||
&& (astUtil.isFunctionLikeExpression(node.value)),
|
||||
instanceVariable: !node.static
|
||||
&& (node.type === 'ClassProperty' || node.type === 'PropertyDefinition')
|
||||
&& (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
|
||||
instanceMethod: !node.static
|
||||
&& (node.type === 'ClassProperty' || node.type === 'PropertyDefinition')
|
||||
&& node.value
|
||||
&& (astUtil.isFunctionLikeExpression(node.value)),
|
||||
typeAnnotation: !!node.typeAnnotation && node.value === null,
|
||||
}));
|
||||
|
||||
// Loop around the properties
|
||||
propertiesInfos.forEach((propA, i) => {
|
||||
// Loop around the properties a second time (for comparison)
|
||||
propertiesInfos.forEach((propB, k) => {
|
||||
if (i === k) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare the properties order
|
||||
const order = comparePropsOrder(propertiesInfos, propA, propB);
|
||||
|
||||
if (!order.correct) {
|
||||
// Store an error if the order is incorrect
|
||||
storeError({
|
||||
node: properties[i],
|
||||
index: order.indexA,
|
||||
}, {
|
||||
node: properties[k],
|
||||
index: order.indexB,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'Program:exit'() {
|
||||
values(components.list()).forEach((component) => {
|
||||
const properties = astUtil.getComponentProperties(component.node);
|
||||
checkPropsOrder(properties);
|
||||
});
|
||||
|
||||
reportErrors();
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
defaultConfig,
|
||||
};
|
||||
180
node_modules/eslint-plugin-react/lib/rules/sort-default-props.js
generated
vendored
Normal file
180
node_modules/eslint-plugin-react/lib/rules/sort-default-props.js
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @fileoverview Enforce default props alphabetical sorting
|
||||
* @author Vladimir Kattsov
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const variableUtil = require('../util/variable');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getFirstTokens = eslintUtil.getFirstTokens;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
propsNotSorted: 'Default prop types declarations should be sorted alphabetically',
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce defaultProps declarations alphabetical sorting',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('sort-default-props'),
|
||||
},
|
||||
// fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoreCase: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
|
||||
/**
|
||||
* Get properties name
|
||||
* @param {Object} node - Property.
|
||||
* @returns {string} Property name.
|
||||
*/
|
||||
function getPropertyName(node) {
|
||||
if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
|
||||
return node.key.name;
|
||||
}
|
||||
if (node.type === 'MemberExpression') {
|
||||
return node.property.name;
|
||||
// Special case for class properties
|
||||
// (babel-eslint@5 does not expose property name so we have to rely on tokens)
|
||||
}
|
||||
if (node.type === 'ClassProperty') {
|
||||
const tokens = getFirstTokens(context, node, 2);
|
||||
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Identifier node passed in looks like a defaultProps declaration.
|
||||
* @param {ASTNode} node The node to check. Must be an Identifier node.
|
||||
* @returns {boolean} `true` if the node is a defaultProps declaration, `false` if not
|
||||
*/
|
||||
function isDefaultPropsDeclaration(node) {
|
||||
const propName = getPropertyName(node);
|
||||
return (propName === 'defaultProps' || propName === 'getDefaultProps');
|
||||
}
|
||||
|
||||
function getKey(node) {
|
||||
return getText(context, node.key || node.argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable by name in the current scope.
|
||||
* @param {ASTNode} node The node to look for.
|
||||
* @param {string} name Name of the variable to look for.
|
||||
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
|
||||
*/
|
||||
function findVariableByName(node, name) {
|
||||
const variable = variableUtil.getVariableFromContext(context, node, name);
|
||||
|
||||
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (variable.defs[0].node.type === 'TypeAlias') {
|
||||
return variable.defs[0].node.right;
|
||||
}
|
||||
|
||||
return variable.defs[0].node.init;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if defaultProps declarations are sorted
|
||||
* @param {Array} declarations The array of AST nodes being checked.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSorted(declarations) {
|
||||
// function fix(fixer) {
|
||||
// return propTypesSortUtil.fixPropTypesSort(context, fixer, declarations, ignoreCase);
|
||||
// }
|
||||
|
||||
declarations.reduce((prev, curr, idx, decls) => {
|
||||
if (/Spread(?:Property|Element)$/.test(curr.type)) {
|
||||
return decls[idx + 1];
|
||||
}
|
||||
|
||||
let prevPropName = getKey(prev);
|
||||
let currentPropName = getKey(curr);
|
||||
|
||||
if (ignoreCase) {
|
||||
prevPropName = prevPropName.toLowerCase();
|
||||
currentPropName = currentPropName.toLowerCase();
|
||||
}
|
||||
|
||||
if (currentPropName < prevPropName) {
|
||||
report(context, messages.propsNotSorted, 'propsNotSorted', {
|
||||
node: curr,
|
||||
// fix
|
||||
});
|
||||
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
}, declarations[0]);
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.type === 'ObjectExpression') {
|
||||
checkSorted(node.properties);
|
||||
} else if (node.type === 'Identifier') {
|
||||
const propTypesObject = findVariableByName(node, node.name);
|
||||
if (propTypesObject && propTypesObject.properties) {
|
||||
checkSorted(propTypesObject.properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Public API
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!isDefaultPropsDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode(node.value);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!isDefaultPropsDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode('right' in node.parent && node.parent.right);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
316
node_modules/eslint-plugin-react/lib/rules/sort-prop-types.js
generated
vendored
Normal file
316
node_modules/eslint-plugin-react/lib/rules/sort-prop-types.js
generated
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* @fileoverview Enforce propTypes declarations alphabetical sorting
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const astUtil = require('../util/ast');
|
||||
const variableUtil = require('../util/variable');
|
||||
const propsUtil = require('../util/props');
|
||||
const docsUrl = require('../util/docsUrl');
|
||||
const propWrapperUtil = require('../util/propWrapper');
|
||||
const propTypesSortUtil = require('../util/propTypesSort');
|
||||
const report = require('../util/report');
|
||||
const eslintUtil = require('../util/eslint');
|
||||
|
||||
const getSourceCode = eslintUtil.getSourceCode;
|
||||
const getText = eslintUtil.getText;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
requiredPropsFirst: 'Required prop types must be listed before all other prop types',
|
||||
callbackPropsLast: 'Callback prop types must be listed after all other prop types',
|
||||
propsNotSorted: 'Prop types declarations should be sorted alphabetically',
|
||||
};
|
||||
|
||||
function getKey(context, node) {
|
||||
if (node.type === 'ObjectTypeProperty') {
|
||||
return getSourceCode(context).getFirstToken(node).value;
|
||||
}
|
||||
if (node.key && node.key.value) {
|
||||
return node.key.value;
|
||||
}
|
||||
return getText(context, node.key || node.argument);
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Enforce propTypes declarations alphabetical sorting',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
url: docsUrl('sort-prop-types'),
|
||||
},
|
||||
fixable: 'code',
|
||||
|
||||
messages,
|
||||
|
||||
schema: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
requiredFirst: {
|
||||
type: 'boolean',
|
||||
},
|
||||
callbacksLast: {
|
||||
type: 'boolean',
|
||||
},
|
||||
ignoreCase: {
|
||||
type: 'boolean',
|
||||
},
|
||||
// Whether alphabetical sorting should be enforced
|
||||
noSortAlphabetically: {
|
||||
type: 'boolean',
|
||||
},
|
||||
sortShapeProp: {
|
||||
type: 'boolean',
|
||||
},
|
||||
checkTypes: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const configuration = context.options[0] || {};
|
||||
const requiredFirst = configuration.requiredFirst || false;
|
||||
const callbacksLast = configuration.callbacksLast || false;
|
||||
const ignoreCase = configuration.ignoreCase || false;
|
||||
const noSortAlphabetically = configuration.noSortAlphabetically || false;
|
||||
const sortShapeProp = configuration.sortShapeProp || false;
|
||||
const checkTypes = configuration.checkTypes || false;
|
||||
|
||||
const typeAnnotations = new Map();
|
||||
|
||||
/**
|
||||
* Checks if propTypes declarations are sorted
|
||||
* @param {Array} declarations The array of AST nodes being checked.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSorted(declarations) {
|
||||
// Declarations will be `undefined` if the `shape` is not a literal. For
|
||||
// example, if it is a propType imported from another file.
|
||||
if (!declarations) {
|
||||
return;
|
||||
}
|
||||
|
||||
function fix(fixer) {
|
||||
return propTypesSortUtil.fixPropTypesSort(
|
||||
context,
|
||||
fixer,
|
||||
declarations,
|
||||
ignoreCase,
|
||||
requiredFirst,
|
||||
callbacksLast,
|
||||
noSortAlphabetically,
|
||||
sortShapeProp,
|
||||
checkTypes
|
||||
);
|
||||
}
|
||||
|
||||
const callbackPropsLastSeen = new WeakSet();
|
||||
const requiredPropsFirstSeen = new WeakSet();
|
||||
const propsNotSortedSeen = new WeakSet();
|
||||
|
||||
declarations.reduce((prev, curr, idx, decls) => {
|
||||
if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
|
||||
return decls[idx + 1];
|
||||
}
|
||||
|
||||
let prevPropName = getKey(context, prev);
|
||||
let currentPropName = getKey(context, curr);
|
||||
const previousIsRequired = propTypesSortUtil.isRequiredProp(prev);
|
||||
const currentIsRequired = propTypesSortUtil.isRequiredProp(curr);
|
||||
const previousIsCallback = propTypesSortUtil.isCallbackPropName(prevPropName);
|
||||
const currentIsCallback = propTypesSortUtil.isCallbackPropName(currentPropName);
|
||||
|
||||
if (ignoreCase) {
|
||||
prevPropName = String(prevPropName).toLowerCase();
|
||||
currentPropName = String(currentPropName).toLowerCase();
|
||||
}
|
||||
|
||||
if (requiredFirst) {
|
||||
if (previousIsRequired && !currentIsRequired) {
|
||||
// Transition between required and non-required. Don't compare for alphabetical.
|
||||
return curr;
|
||||
}
|
||||
if (!previousIsRequired && currentIsRequired) {
|
||||
// Encountered a non-required prop after a required prop
|
||||
if (!requiredPropsFirstSeen.has(curr)) {
|
||||
requiredPropsFirstSeen.add(curr);
|
||||
report(context, messages.requiredPropsFirst, 'requiredPropsFirst', {
|
||||
node: curr,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
if (callbacksLast) {
|
||||
if (!previousIsCallback && currentIsCallback) {
|
||||
// Entering the callback prop section
|
||||
return curr;
|
||||
}
|
||||
if (previousIsCallback && !currentIsCallback) {
|
||||
// Encountered a non-callback prop after a callback prop
|
||||
if (!callbackPropsLastSeen.has(prev)) {
|
||||
callbackPropsLastSeen.add(prev);
|
||||
report(context, messages.callbackPropsLast, 'callbackPropsLast', {
|
||||
node: prev,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
if (!noSortAlphabetically && currentPropName < prevPropName) {
|
||||
if (!propsNotSortedSeen.has(curr)) {
|
||||
propsNotSortedSeen.add(curr);
|
||||
report(context, messages.propsNotSorted, 'propsNotSorted', {
|
||||
node: curr,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
}, declarations[0]);
|
||||
}
|
||||
|
||||
function checkNode(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ObjectExpression') {
|
||||
checkSorted(node.properties);
|
||||
} else if (node.type === 'Identifier') {
|
||||
const propTypesObject = variableUtil.findVariableByName(context, node, node.name);
|
||||
if (propTypesObject && propTypesObject.properties) {
|
||||
checkSorted(propTypesObject.properties);
|
||||
}
|
||||
} else if (astUtil.isCallExpression(node)) {
|
||||
const innerNode = node.arguments && node.arguments[0];
|
||||
if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) {
|
||||
checkNode(innerNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleFunctionComponent(node) {
|
||||
const firstArg = node.params
|
||||
&& node.params.length > 0
|
||||
&& node.params[0].typeAnnotation
|
||||
&& node.params[0].typeAnnotation.typeAnnotation;
|
||||
if (firstArg && firstArg.type === 'TSTypeReference') {
|
||||
const propType = typeAnnotations.get(firstArg.typeName.name)
|
||||
&& typeAnnotations.get(firstArg.typeName.name)[0];
|
||||
if (propType && propType.members) {
|
||||
checkSorted(propType.members);
|
||||
}
|
||||
} else if (firstArg && firstArg.type === 'TSTypeLiteral') {
|
||||
if (firstArg.members) {
|
||||
checkSorted(firstArg.members);
|
||||
}
|
||||
} else if (firstArg && firstArg.type === 'GenericTypeAnnotation') {
|
||||
const propType = typeAnnotations.get(firstArg.id.name)
|
||||
&& typeAnnotations.get(firstArg.id.name)[0];
|
||||
if (propType && propType.properties) {
|
||||
checkSorted(propType.properties);
|
||||
}
|
||||
} else if (firstArg && firstArg.type === 'ObjectTypeAnnotation') {
|
||||
if (firstArg.properties) {
|
||||
checkSorted(firstArg.properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
CallExpression(node) {
|
||||
if (!sortShapeProp || !propTypesSortUtil.isShapeProp(node) || !(node.arguments && node.arguments[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstArg = node.arguments[0];
|
||||
if (firstArg.properties) {
|
||||
checkSorted(firstArg.properties);
|
||||
} else if (firstArg.type === 'Identifier') {
|
||||
const variable = variableUtil.findVariableByName(context, node, firstArg.name);
|
||||
if (variable && variable.properties) {
|
||||
checkSorted(variable.properties);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ClassProperty, PropertyDefinition'(node) {
|
||||
if (!propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
checkNode(node.value);
|
||||
},
|
||||
|
||||
MemberExpression(node) {
|
||||
if (!propsUtil.isPropTypesDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNode(node.parent.right);
|
||||
},
|
||||
|
||||
ObjectExpression(node) {
|
||||
node.properties.forEach((property) => {
|
||||
if (!property.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!propsUtil.isPropTypesDeclaration(property)) {
|
||||
return;
|
||||
}
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
checkSorted(property.value.properties);
|
||||
}
|
||||
});
|
||||
},
|
||||
}, checkTypes ? {
|
||||
TSTypeLiteral(node) {
|
||||
if (node && node.parent.id) {
|
||||
const currentNode = [].concat(
|
||||
typeAnnotations.get(node.parent.id.name) || [],
|
||||
node
|
||||
);
|
||||
typeAnnotations.set(node.parent.id.name, currentNode);
|
||||
}
|
||||
},
|
||||
|
||||
TypeAlias(node) {
|
||||
if (node.right.type === 'ObjectTypeAnnotation') {
|
||||
const currentNode = [].concat(
|
||||
typeAnnotations.get(node.id.name) || [],
|
||||
node.right
|
||||
);
|
||||
typeAnnotations.set(node.id.name, currentNode);
|
||||
}
|
||||
},
|
||||
|
||||
TSTypeAliasDeclaration(node) {
|
||||
if (node.typeAnnotation.type === 'TSTypeLiteral' || node.typeAnnotation.type === 'ObjectTypeAnnotation') {
|
||||
const currentNode = [].concat(
|
||||
typeAnnotations.get(node.id.name) || [],
|
||||
node.typeAnnotation
|
||||
);
|
||||
typeAnnotations.set(node.id.name, currentNode);
|
||||
}
|
||||
},
|
||||
FunctionDeclaration: handleFunctionComponent,
|
||||
ArrowFunctionExpression: handleFunctionComponent,
|
||||
} : null);
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user