UNPKG

6.29 kBJavaScriptView Raw
1/**
2 * @fileoverview Prevent missing props validation in a React component definition
3 * @author Yannick Croissant
4 */
5
6'use strict';
7
8// As for exceptions for props.children or props.className (and alike) look at
9// https://github.com/yannickcr/eslint-plugin-react/issues/7
10
11const Components = require('../util/Components');
12const docsUrl = require('../util/docsUrl');
13
14// ------------------------------------------------------------------------------
15// Rule Definition
16// ------------------------------------------------------------------------------
17
18module.exports = {
19 meta: {
20 docs: {
21 description: 'Prevent missing props validation in a React component definition',
22 category: 'Best Practices',
23 recommended: true,
24 url: docsUrl('prop-types')
25 },
26
27 schema: [{
28 type: 'object',
29 properties: {
30 ignore: {
31 type: 'array',
32 items: {
33 type: 'string'
34 }
35 },
36 customValidators: {
37 type: 'array',
38 items: {
39 type: 'string'
40 }
41 },
42 skipUndeclared: {
43 type: 'boolean'
44 }
45 },
46 additionalProperties: false
47 }]
48 },
49
50 create: Components.detect((context, components) => {
51 const configuration = context.options[0] || {};
52 const ignored = configuration.ignore || [];
53 const skipUndeclared = configuration.skipUndeclared || false;
54
55 const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation';
56
57 /**
58 * Checks if the prop is ignored
59 * @param {String} name Name of the prop to check.
60 * @returns {Boolean} True if the prop is ignored, false if not.
61 */
62 function isIgnored(name) {
63 return ignored.indexOf(name) !== -1;
64 }
65
66 /**
67 * Checks if the component must be validated
68 * @param {Object} component The component to process
69 * @returns {Boolean} True if the component must be validated, false if not.
70 */
71 function mustBeValidated(component) {
72 const isSkippedByConfig = skipUndeclared && typeof component.declaredPropTypes === 'undefined';
73 return Boolean(
74 component
75 && component.usedPropTypes
76 && !component.ignorePropsValidation
77 && !isSkippedByConfig
78 );
79 }
80
81 /**
82 * Internal: Checks if the prop is declared
83 * @param {Object} declaredPropTypes Description of propTypes declared in the current component
84 * @param {String[]} keyList Dot separated name of the prop to check.
85 * @returns {Boolean} True if the prop is declared, false if not.
86 */
87 function internalIsDeclaredInComponent(declaredPropTypes, keyList) {
88 for (let i = 0, j = keyList.length; i < j; i++) {
89 const key = keyList[i];
90 const propType = (
91 declaredPropTypes && (
92 // Check if this key is declared
93 (declaredPropTypes[key] // If not, check if this type accepts any key
94 || declaredPropTypes.__ANY_KEY__) // eslint-disable-line no-underscore-dangle
95 )
96 );
97
98 if (!propType) {
99 // If it's a computed property, we can't make any further analysis, but is valid
100 return key === '__COMPUTED_PROP__';
101 }
102 if (typeof propType === 'object' && !propType.type) {
103 return true;
104 }
105 // Consider every children as declared
106 if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) {
107 return true;
108 }
109 if (propType.acceptedProperties) {
110 return key in propType.acceptedProperties;
111 }
112 if (propType.type === 'union') {
113 // If we fall in this case, we know there is at least one complex type in the union
114 if (i + 1 >= j) {
115 // this is the last key, accept everything
116 return true;
117 }
118 // non trivial, check all of them
119 const unionTypes = propType.children;
120 const unionPropType = {};
121 for (let k = 0, z = unionTypes.length; k < z; k++) {
122 unionPropType[key] = unionTypes[k];
123 const isValid = internalIsDeclaredInComponent(
124 unionPropType,
125 keyList.slice(i)
126 );
127 if (isValid) {
128 return true;
129 }
130 }
131
132 // every possible union were invalid
133 return false;
134 }
135 declaredPropTypes = propType.children;
136 }
137 return true;
138 }
139
140 /**
141 * Checks if the prop is declared
142 * @param {ASTNode} node The AST node being checked.
143 * @param {String[]} names List of names of the prop to check.
144 * @returns {Boolean} True if the prop is declared, false if not.
145 */
146 function isDeclaredInComponent(node, names) {
147 while (node) {
148 const component = components.get(node);
149
150 const isDeclared = component && component.confidence === 2
151 && internalIsDeclaredInComponent(component.declaredPropTypes || {}, names);
152 if (isDeclared) {
153 return true;
154 }
155 node = node.parent;
156 }
157 return false;
158 }
159
160 /**
161 * Reports undeclared proptypes for a given component
162 * @param {Object} component The component to process
163 */
164 function reportUndeclaredPropTypes(component) {
165 const undeclareds = component.usedPropTypes.filter((propType) => (
166 propType.node
167 && !isIgnored(propType.allNames[0])
168 && !isDeclaredInComponent(component.node, propType.allNames)
169 ));
170 undeclareds.forEach((propType) => {
171 context.report({
172 node: propType.node,
173 message: MISSING_MESSAGE,
174 data: {
175 name: propType.allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]')
176 }
177 });
178 });
179 }
180
181 // --------------------------------------------------------------------------
182 // Public
183 // --------------------------------------------------------------------------
184
185 return {
186 'Program:exit'() {
187 const list = components.list();
188 // Report undeclared proptypes for all classes
189 Object.keys(list).filter((component) => mustBeValidated(list[component])).forEach((component) => {
190 reportUndeclaredPropTypes(list[component]);
191 });
192 }
193 };
194 })
195};