Skip to content

Commit eb4a47a

Browse files
committed
Add flow support for require-default-props
1 parent 52de665 commit eb4a47a

File tree

4 files changed

+853
-96
lines changed

4 files changed

+853
-96
lines changed

docs/rules/require-default-props.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ class Greeting extends React.Component {
119119
}
120120
```
121121

122+
```js
123+
type Props = {
124+
foo: string,
125+
bar?: string
126+
};
127+
128+
function MyStatelessComponent(props: Props) {
129+
return <div>Hello {props.foo} {props.bar}</div>;
130+
}
131+
```
132+
122133
The following patterns are not considered warnings:
123134

124135
```js
@@ -147,6 +158,21 @@ MyStatelessComponent.defaultProps = {
147158
};
148159
```
149160

161+
```js
162+
type Props = {
163+
foo: string,
164+
bar?: string
165+
};
166+
167+
function MyStatelessComponent(props: Props) {
168+
return <div>Hello {props.foo} {props.bar}</div>;
169+
}
170+
171+
MyStatelessComponent.defaultProps = {
172+
bar: 'some default'
173+
};
174+
```
175+
150176
```js
151177
function NotAComponent({ foo, bar }) {}
152178

lib/rules/require-default-props.js

Lines changed: 220 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -6,78 +6,7 @@
66

77
var Components = require('../util/Components');
88
var variableUtil = require('../util/variable');
9-
10-
// ------------------------------------------------------------------------------
11-
// Helpers
12-
// ------------------------------------------------------------------------------
13-
14-
/**
15-
* Checks if the Identifier node passed in looks like a propTypes declaration.
16-
* @param {ASTNode} node The node to check. Must be an Identifier node.
17-
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
18-
*/
19-
function isPropTypesDeclaration(node) {
20-
return node.type === 'Identifier' && node.name === 'propTypes';
21-
}
22-
23-
/**
24-
* Checks if the Identifier node passed in looks like a defaultProps declaration.
25-
* @param {ASTNode} node The node to check. Must be an Identifier node.
26-
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
27-
*/
28-
function isDefaultPropsDeclaration(node) {
29-
return node.type === 'Identifier' &&
30-
(node.name === 'defaultProps' || node.name === 'getDefaultProps');
31-
}
32-
33-
/**
34-
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
35-
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
36-
* @returns {Boolean} `true` if this PropType is required, `false` if not.
37-
*/
38-
function isRequiredPropType(propTypeExpression) {
39-
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
40-
}
41-
42-
/**
43-
* Extracts a PropType from an ObjectExpression node.
44-
* @param {ASTNode} objectExpression ObjectExpression node.
45-
* @returns {Object} Object representation of a PropType, to be consumed by `addPropTypesToComponent`.
46-
*/
47-
function getPropTypesFromObjectExpression(objectExpression) {
48-
var props = objectExpression.properties.filter(function(property) {
49-
return property.type !== 'ExperimentalSpreadProperty';
50-
});
51-
52-
return props.map(function(property) {
53-
return {
54-
name: property.key.name,
55-
isRequired: isRequiredPropType(property.value),
56-
node: property
57-
};
58-
});
59-
}
60-
61-
/**
62-
* Extracts a DefaultProp from an ObjectExpression node.
63-
* @param {ASTNode} objectExpression ObjectExpression node.
64-
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
65-
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
66-
* from this ObjectExpression can't be resolved.
67-
*/
68-
function getDefaultPropsFromObjectExpression(objectExpression) {
69-
var hasSpread = objectExpression.properties.find(function(property) {
70-
return property.type === 'ExperimentalSpreadProperty';
71-
});
72-
73-
if (hasSpread) {
74-
return 'unresolved';
75-
}
76-
77-
return objectExpression.properties.map(function(property) {
78-
return property.key.name;
79-
});
80-
}
9+
var annotations = require('../util/annotations');
8110

8211
// ------------------------------------------------------------------------------
8312
// Rule Definition
@@ -94,6 +23,189 @@ module.exports = {
9423
},
9524

9625
create: Components.detect(function(context, components, utils) {
26+
/**
27+
* Checks if the Identifier node passed in looks like a propTypes declaration.
28+
* @param {ASTNode} node The node to check. Must be an Identifier node.
29+
* @returns {Boolean} `true` if the node is a propTypes declaration, `false` if not
30+
*/
31+
function isPropTypesDeclaration(node) {
32+
return node.type === 'Identifier' && node.name === 'propTypes';
33+
}
34+
35+
/**
36+
* Checks if the Identifier node passed in looks like a defaultProps declaration.
37+
* @param {ASTNode} node The node to check. Must be an Identifier node.
38+
* @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not
39+
*/
40+
function isDefaultPropsDeclaration(node) {
41+
return node.type === 'Identifier' &&
42+
(node.name === 'defaultProps' || node.name === 'getDefaultProps');
43+
}
44+
45+
/**
46+
* Checks if the PropTypes MemberExpression node passed in declares a required propType.
47+
* @param {ASTNode} propTypeExpression node to check. Must be a `PropTypes` MemberExpression.
48+
* @returns {Boolean} `true` if this PropType is required, `false` if not.
49+
*/
50+
function isRequiredPropType(propTypeExpression) {
51+
return propTypeExpression.type === 'MemberExpression' && propTypeExpression.property.name === 'isRequired';
52+
}
53+
54+
/**
55+
* Find a variable by name in the current scope.
56+
* @param {string} name Name of the variable to look for.
57+
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
58+
*/
59+
function findVariableByName(name) {
60+
var variable = variableUtil.variablesInScope(context).find(function(item) {
61+
return item.name === name;
62+
});
63+
64+
if (!variable || !variable.defs[0] || !variable.defs[0].node) {
65+
return null;
66+
}
67+
68+
if (variable.defs[0].node.type === 'TypeAlias') {
69+
return variable.defs[0].node.right;
70+
}
71+
72+
// FIXME(vitorbal): is this needed?
73+
if (!variable.defs[0].node.init) {
74+
return null;
75+
}
76+
77+
return variable.defs[0].node.init;
78+
}
79+
80+
/**
81+
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
82+
* an Identifier, then the node is simply returned.
83+
* @param {ASTNode} node The node to resolve.
84+
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
85+
*/
86+
function resolveNodeValue(node) {
87+
if (node.type === 'Identifier') {
88+
return findVariableByName(node.name);
89+
}
90+
91+
return node;
92+
}
93+
94+
/**
95+
* Tries to find the definition of a GenericTypeAnnotation in the current scope.
96+
* @param {ASTNode} node The node GenericTypeAnnotation node to resolve.
97+
* @return {ASTNode|null} Return null if definition cannot be found, ASTNode otherwise.
98+
*/
99+
function resolveGenericTypeAnnotation(node) {
100+
if (node.type !== 'GenericTypeAnnotation' || node.id.type !== 'Identifier') {
101+
return null;
102+
}
103+
104+
return findVariableByName(node.id.name);
105+
}
106+
107+
function resolveUnionTypeAnnotation(node) {
108+
// Go through all the union and resolve any generic types.
109+
return node.types.map(function(annotation) {
110+
if (annotation.type === 'GenericTypeAnnotation') {
111+
return resolveGenericTypeAnnotation(annotation);
112+
}
113+
114+
return annotation;
115+
});
116+
}
117+
118+
/**
119+
* Extracts a PropType from an ObjectExpression node.
120+
* @param {ASTNode} objectExpression ObjectExpression node.
121+
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
122+
*/
123+
function getPropTypesFromObjectExpression(objectExpression) {
124+
var props = objectExpression.properties.filter(function(property) {
125+
return property.type !== 'ExperimentalSpreadProperty';
126+
});
127+
128+
return props.map(function(property) {
129+
return {
130+
name: property.key.name,
131+
isRequired: isRequiredPropType(property.value),
132+
node: property
133+
};
134+
});
135+
}
136+
137+
/**
138+
* Extracts a PropType from a TypeAnnotation node.
139+
* @param {ASTNode} node TypeAnnotation node.
140+
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
141+
*/
142+
function getPropTypesFromTypeAnnotation(node) {
143+
var properties;
144+
145+
switch (node.typeAnnotation.type) {
146+
case 'GenericTypeAnnotation':
147+
var annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
148+
properties = annotation ? annotation.properties : [];
149+
break;
150+
151+
case 'UnionTypeAnnotation':
152+
var union = resolveUnionTypeAnnotation(node.typeAnnotation);
153+
properties = union.reduce(function(acc, curr) {
154+
if (!curr) {
155+
return acc;
156+
}
157+
158+
return acc.concat(curr.properties);
159+
}, []);
160+
break;
161+
162+
case 'ObjectTypeAnnotation':
163+
properties = node.typeAnnotation.properties;
164+
break;
165+
166+
default:
167+
properties = [];
168+
break;
169+
}
170+
171+
var props = properties.filter(function(property) {
172+
return property.type === 'ObjectTypeProperty';
173+
});
174+
175+
return props.map(function(property) {
176+
// the `key` property is not present in ObjectTypeProperty nodes, so we need to get the key name manually.
177+
var tokens = context.getFirstTokens(property, 1);
178+
var name = tokens[0].value;
179+
180+
return {
181+
name: name,
182+
isRequired: !property.optional,
183+
node: property
184+
};
185+
});
186+
}
187+
188+
/**
189+
* Extracts a DefaultProp from an ObjectExpression node.
190+
* @param {ASTNode} objectExpression ObjectExpression node.
191+
* @returns {Object|string} Object representation of a defaultProp, to be consumed by
192+
* `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps
193+
* from this ObjectExpression can't be resolved.
194+
*/
195+
function getDefaultPropsFromObjectExpression(objectExpression) {
196+
var hasSpread = objectExpression.properties.find(function(property) {
197+
return property.type === 'ExperimentalSpreadProperty';
198+
});
199+
200+
if (hasSpread) {
201+
return 'unresolved';
202+
}
203+
204+
return objectExpression.properties.map(function(property) {
205+
return property.key.name;
206+
});
207+
}
208+
97209
/**
98210
* Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is
99211
* marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations
@@ -151,34 +263,36 @@ module.exports = {
151263
}
152264

153265
/**
154-
* Find a variable by name in the current scope.
155-
* @param {string} name Name of the variable to look for.
156-
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
266+
* Tries to find a props type annotation in a stateless component.
267+
* @param {ASTNode} node The AST node to look for a props type annotation.
268+
* @return {void}
157269
*/
158-
function findVariableByName(name) {
159-
var variable = variableUtil.variablesInScope(context).find(function(item) {
160-
return item.name === name;
161-
});
270+
function handleStatelessComponent(node) {
271+
if (!node.params || !node.params.length || !annotations.isAnnotatedFunctionPropsDeclaration(node, context)) {
272+
return;
273+
}
162274

163-
if (!variable || !variable.defs[0] || !variable.defs[0].node.init) {
164-
return null;
275+
// find component this props annotation belongs to
276+
var component = components.get(utils.getParentStatelessComponent());
277+
if (!component) {
278+
return;
165279
}
166280

167-
return variable.defs[0].node.init;
281+
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.params[0].typeAnnotation, context));
168282
}
169283

170-
/**
171-
* Try to resolve the node passed in to a variable in the current scope. If the node passed in is not
172-
* an Identifier, then the node is simply returned.
173-
* @param {ASTNode} node The node to resolve.
174-
* @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise.
175-
*/
176-
function resolveNodeValue(node) {
177-
if (node.type === 'Identifier') {
178-
return findVariableByName(node.name);
284+
function handlePropTypeAnnotationClassProperty(node) {
285+
// find component this props annotation belongs to
286+
var component = components.get(utils.getParentES6Component());
287+
if (!component) {
288+
return;
179289
}
180290

181-
return node;
291+
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
292+
}
293+
294+
function isPropTypeAnnotation(node) {
295+
return (node.key.name === 'props' && !!node.typeAnnotation);
182296
}
183297

184298
/**
@@ -344,10 +458,19 @@ module.exports = {
344458
// };
345459
// }
346460
ClassProperty: function(node) {
461+
if (isPropTypeAnnotation(node)) {
462+
handlePropTypeAnnotationClassProperty(node);
463+
return;
464+
}
465+
347466
if (!node.static) {
348467
return;
349468
}
350469

470+
if (!node.value) {
471+
return;
472+
}
473+
351474
var isPropType = isPropTypesDeclaration(node.key);
352475
var isDefaultProp = isDefaultPropsDeclaration(node.key);
353476

@@ -423,6 +546,11 @@ module.exports = {
423546
});
424547
},
425548

549+
// Check for type annotations in stateless components
550+
FunctionDeclaration: handleStatelessComponent,
551+
ArrowFunctionExpression: handleStatelessComponent,
552+
FunctionExpression: handleStatelessComponent,
553+
426554
'Program:exit': function() {
427555
var list = components.list();
428556

0 commit comments

Comments
 (0)