blob: 13fe4ba095db7efa612a9e5f5e6790f917d7a771 [file] [log] [blame]
/**
* @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/yannickcr/eslint-plugin-react/issues/7
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prevent missing props validation in a React component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('prop-types')
},
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;
const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
/**
* 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 Boolean(
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.containsSpread) {
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) => {
context.report({
node: propType.node,
message: MISSING_MESSAGE,
data: {
name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]')
}
});
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
'Program:exit': function () {
const list = components.list();
// Report undeclared proptypes for all classes
Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => {
reportUndeclaredPropTypes(list[component]);
});
}
};
})
};