| /** |
| * @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.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) => { |
| context.report({ |
| node: propType.node, |
| message: MISSING_MESSAGE, |
| data: { |
| name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]') |
| } |
| }); |
| }); |
| } |
| |
| // -------------------------------------------------------------------------- |
| // Public |
| // -------------------------------------------------------------------------- |
| |
| return { |
| 'Program:exit'() { |
| const list = components.list(); |
| // Report undeclared proptypes for all classes |
| Object.keys(list).filter(component => mustBeValidated(list[component])).forEach((component) => { |
| reportUndeclaredPropTypes(list[component]); |
| }); |
| } |
| }; |
| }) |
| }; |