You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

193 lines
6.4 KiB

/**
* @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.',
};
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: {
allow: {
enum: ['defaultProps', 'ignore'],
},
},
functions: {
allow: {
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.type === 'Identifier') {
const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired);
if (hasOptionalProp) {
report(context, messages.destructureInSignature, 'destructureInSignature', {
node: props,
});
}
} else if (props.type === 'ObjectPattern') {
props.properties.filter((prop) => {
if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') {
return false;
}
const propType = propTypes[prop.key.name];
if (!propType || propType.isRequired) {
return false;
}
return prop.value.type !== 'AssignmentPattern';
}).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 || {}
);
}
});
},
};
}),
};