Skip to content

Commit 11dc56b

Browse files
Moroine Bentefritljharb
Moroine Bentefrit
authored andcommitted
[New] prop-types: Support Flow Type spread
Fixes #2138.
1 parent 7fa31df commit 11dc56b

File tree

6 files changed

+242
-10
lines changed

6 files changed

+242
-10
lines changed

lib/rules/prop-types.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ module.exports = {
103103
return true;
104104
}
105105
// Consider every children as declared
106-
if (propType.children === true || propType.containsSpread || propType.containsIndexers) {
106+
if (propType.children === true || propType.containsUnresolvedSpread || propType.containsIndexers) {
107107
return true;
108108
}
109109
if (propType.acceptedProperties) {

lib/util/ast.js

+3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ function getKeyValue(context, node) {
165165
stripQuotes(tokens[0].value)
166166
);
167167
}
168+
if (node.type === 'GenericTypeAnnotation') {
169+
return node.id.name;
170+
}
168171
const key = node.key || node.argument;
169172
return key.type === 'Identifier' ? key.name : key.value;
170173
}

lib/util/propTypes.js

+34-8
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,19 @@ function isSuperTypeParameterPropsDeclaration(node) {
3232
* @param {Object[]} properties Array of properties to iterate.
3333
* @param {Function} fn Function to call on each property, receives property key
3434
and property value. (key, value) => void
35+
* @param {Function} [handleSpreadFn] Function to call on each ObjectTypeSpreadProperty, receives the
36+
argument
3537
*/
36-
function iterateProperties(context, properties, fn) {
38+
function iterateProperties(context, properties, fn, handleSpreadFn) {
3739
if (properties && properties.length && typeof fn === 'function') {
3840
for (let i = 0, j = properties.length; i < j; i++) {
3941
const node = properties[i];
4042
const key = getKeyValue(context, node);
4143

44+
if (node.type === 'ObjectTypeSpreadProperty' && typeof handleSpreadFn === 'function') {
45+
handleSpreadFn(node.argument);
46+
}
47+
4248
const value = node.value;
4349
fn(key, value, node);
4450
}
@@ -121,30 +127,41 @@ module.exports = function propTypesInstructions(context, components, utils) {
121127
},
122128

123129
ObjectTypeAnnotation(annotation, parentName, seen) {
124-
let containsObjectTypeSpread = false;
130+
let containsUnresolvedObjectTypeSpread = false;
131+
let containsSpread = false;
125132
const containsIndexers = Boolean(annotation.indexers && annotation.indexers.length);
126133
const shapeTypeDefinition = {
127134
type: 'shape',
128135
children: {}
129136
};
130137
iterateProperties(context, annotation.properties, (childKey, childValue, propNode) => {
131138
const fullName = [parentName, childKey].join('.');
132-
if (!childKey && !childValue) {
133-
containsObjectTypeSpread = true;
134-
} else {
139+
if (childKey || childValue) {
135140
const types = buildTypeAnnotationDeclarationTypes(childValue, fullName, seen);
136141
types.fullName = fullName;
137142
types.name = childKey;
138143
types.node = propNode;
139144
types.isRequired = !childValue.optional;
140145
shapeTypeDefinition.children[childKey] = types;
141146
}
147+
},
148+
(spreadNode) => {
149+
const key = getKeyValue(context, spreadNode);
150+
const types = buildTypeAnnotationDeclarationTypes(spreadNode, key, seen);
151+
if (!types.children) {
152+
containsUnresolvedObjectTypeSpread = true;
153+
} else {
154+
Object.assign(shapeTypeDefinition, types.children);
155+
}
156+
containsSpread = true;
142157
});
143158

144159
// Mark if this shape has spread or an indexer. We will know to consider all props from this shape as having propTypes,
145160
// but still have the ability to detect unused children of this shape.
146-
shapeTypeDefinition.containsSpread = containsObjectTypeSpread;
161+
shapeTypeDefinition.containsUnresolvedSpread = containsUnresolvedObjectTypeSpread;
147162
shapeTypeDefinition.containsIndexers = containsIndexers;
163+
// Deprecated: containsSpread is not used anymore in the codebase, ensure to keep API backward compatibility
164+
shapeTypeDefinition.containsSpread = containsSpread;
148165

149166
return shapeTypeDefinition;
150167
},
@@ -241,7 +258,7 @@ module.exports = function propTypesInstructions(context, components, utils) {
241258
}
242259

243260
/**
244-
* Marks all props found inside ObjectTypeAnnotaiton as declared.
261+
* Marks all props found inside ObjectTypeAnnotation as declared.
245262
*
246263
* Modifies the declaredProperties object
247264
* @param {ASTNode} propTypes
@@ -253,7 +270,7 @@ module.exports = function propTypesInstructions(context, components, utils) {
253270

254271
iterateProperties(context, propTypes.properties, (key, value, propNode) => {
255272
if (!value) {
256-
ignorePropsValidation = true;
273+
ignorePropsValidation = ignorePropsValidation || propNode.type !== 'ObjectTypeSpreadProperty';
257274
return;
258275
}
259276

@@ -263,6 +280,15 @@ module.exports = function propTypesInstructions(context, components, utils) {
263280
types.node = propNode;
264281
types.isRequired = !propNode.optional;
265282
declaredPropTypes[key] = types;
283+
}, (spreadNode) => {
284+
const key = getKeyValue(context, spreadNode);
285+
const spreadAnnotation = getInTypeScope(key);
286+
if (!spreadAnnotation) {
287+
ignorePropsValidation = true;
288+
} else {
289+
const spreadIgnoreValidation = declarePropTypesForObjectTypeAnnotation(spreadAnnotation, declaredPropTypes);
290+
ignorePropsValidation = ignorePropsValidation || spreadIgnoreValidation;
291+
}
266292
});
267293

268294
return ignorePropsValidation;

tests/lib/rules/default-props-match-prop-types.js

+116-1
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,58 @@ ruleTester.run('default-props-match-prop-types', rule, {
733733
].join('\n'),
734734
parser: parsers.BABEL_ESLINT
735735
},
736+
{
737+
code: [
738+
'type DefaultProps1 = {|',
739+
' bar1?: string',
740+
'|};',
741+
'type DefaultProps2 = {|',
742+
' ...DefaultProps1,',
743+
' bar2?: string',
744+
'|};',
745+
'type Props = {',
746+
' foo: string,',
747+
' ...DefaultProps2',
748+
'};',
749+
750+
'function Hello(props: Props) {',
751+
' return <div>Hello {props.foo}</div>;',
752+
'}',
753+
754+
'Hello.defaultProps = {',
755+
' bar1: "bar1",',
756+
' bar2: "bar2",',
757+
'};'
758+
].join('\n'),
759+
parser: parsers.BABEL_ESLINT
760+
},
761+
{
762+
code: [
763+
'type DefaultProps1 = {|',
764+
' bar1?: string',
765+
'|};',
766+
'type DefaultProps2 = {|',
767+
' ...DefaultProps1,',
768+
' bar2?: string',
769+
'|};',
770+
'type Props = {',
771+
' foo: string,',
772+
' ...DefaultProps2',
773+
'};',
774+
775+
'class Hello extends React.Component<Props> {',
776+
' render() {',
777+
' return <div>Hello {props.foo}</div>;',
778+
' }',
779+
'}',
780+
781+
'Hello.defaultProps = {',
782+
' bar1: "bar1",',
783+
' bar2: "bar2",',
784+
'};'
785+
].join('\n'),
786+
parser: parsers.BABEL_ESLINT
787+
},
736788
// don't error when variable is not in scope
737789
{
738790
code: [
@@ -1460,7 +1512,6 @@ ruleTester.run('default-props-match-prop-types', rule, {
14601512
column: 3
14611513
}]
14621514
},
1463-
// Investigate why this test fails. Flow type not finding foo?
14641515
{
14651516
code: [
14661517
'function Hello(props: { foo: string }) {',
@@ -1590,6 +1641,70 @@ ruleTester.run('default-props-match-prop-types', rule, {
15901641
message: 'defaultProp "firstProperty" defined for isRequired propType.'
15911642
}
15921643
]
1644+
},
1645+
{
1646+
code: [
1647+
'type DefaultProps = {',
1648+
' baz?: string,',
1649+
' bar?: string',
1650+
'};',
1651+
1652+
'type Props = {',
1653+
' foo: string,',
1654+
' ...DefaultProps',
1655+
'}',
1656+
1657+
'function Hello(props: Props) {',
1658+
' return <div>Hello {props.foo}</div>;',
1659+
'}',
1660+
'Hello.defaultProps = { foo: "foo", frob: "frob", baz: "bar" };'
1661+
].join('\n'),
1662+
parser: parsers.BABEL_ESLINT,
1663+
errors: [
1664+
{
1665+
message: 'defaultProp "foo" defined for isRequired propType.',
1666+
line: 12,
1667+
column: 24
1668+
},
1669+
{
1670+
message: 'defaultProp "frob" has no corresponding propTypes declaration.',
1671+
line: 12,
1672+
column: 36
1673+
}
1674+
]
1675+
},
1676+
{
1677+
code: [
1678+
'type DefaultProps = {',
1679+
' baz?: string,',
1680+
' bar?: string',
1681+
'};',
1682+
1683+
'type Props = {',
1684+
' foo: string,',
1685+
' ...DefaultProps',
1686+
'}',
1687+
1688+
'class Hello extends React.Component<Props> {',
1689+
' render() {',
1690+
' return <div>Hello {props.foo}</div>;',
1691+
' }',
1692+
'}',
1693+
'Hello.defaultProps = { foo: "foo", frob: "frob", baz: "bar" };'
1694+
].join('\n'),
1695+
parser: parsers.BABEL_ESLINT,
1696+
errors: [
1697+
{
1698+
message: 'defaultProp "foo" defined for isRequired propType.',
1699+
line: 14,
1700+
column: 24
1701+
},
1702+
{
1703+
message: 'defaultProp "frob" has no corresponding propTypes declaration.',
1704+
line: 14,
1705+
column: 36
1706+
}
1707+
]
15931708
}
15941709
]
15951710
});

tests/lib/rules/no-unused-prop-types.js

+23
Original file line numberDiff line numberDiff line change
@@ -4619,6 +4619,29 @@ ruleTester.run('no-unused-prop-types', rule, {
46194619
errors: [{
46204620
message: '\'aProp\' PropType is defined but prop is never used'
46214621
}]
4622+
}, {
4623+
// issue #2138
4624+
code: `
4625+
type UsedProps = {|
4626+
usedProp: number,
4627+
|};
4628+
4629+
type UnusedProps = {|
4630+
unusedProp: number,
4631+
|};
4632+
4633+
type Props = {| ...UsedProps, ...UnusedProps |};
4634+
4635+
function MyComponent({ usedProp, notOne }: Props) {
4636+
return <div>{usedProp}</div>;
4637+
}
4638+
`,
4639+
parser: parsers.BABEL_ESLINT,
4640+
errors: [{
4641+
message: "'unusedProp' PropType is defined but prop is never used",
4642+
line: 7,
4643+
column: 23
4644+
}]
46224645
}, {
46234646
code: `
46244647
type Props = {

tests/lib/rules/prop-types.js

+65
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,23 @@ ruleTester.run('prop-types', rule, {
16321632
'}'
16331633
].join('\n'),
16341634
parser: parsers.BABEL_ESLINT
1635+
}, {
1636+
code: [
1637+
'type OtherProps = {',
1638+
' firstname: string,',
1639+
'};',
1640+
'type Props = {',
1641+
' ...OtherProps,',
1642+
' lastname: string',
1643+
'};',
1644+
'class Hello extends React.Component {',
1645+
' props: Props;',
1646+
' render () {',
1647+
' return <div>Hello {this.props.firstname}</div>;',
1648+
' }',
1649+
'}'
1650+
].join('\n'),
1651+
parser: parsers.BABEL_ESLINT
16351652
}, {
16361653
code: [
16371654
'type Person = {',
@@ -2335,6 +2352,30 @@ ruleTester.run('prop-types', rule, {
23352352
`,
23362353
parser: parsers.BABEL_ESLINT
23372354
},
2355+
{
2356+
// issue #2138
2357+
code: `
2358+
type UsedProps = {|
2359+
usedProp: number,
2360+
|};
2361+
2362+
type UnusedProps = {|
2363+
unusedProp: number,
2364+
|};
2365+
2366+
type Props = {| ...UsedProps, ...UnusedProps |};
2367+
2368+
function MyComponent({ usedProp }: Props) {
2369+
return <div>{usedProp}</div>;
2370+
}
2371+
`,
2372+
parser: parsers.BABEL_ESLINT,
2373+
errors: [{
2374+
message: "'notOne' is missing in props validation",
2375+
line: 8,
2376+
column: 34
2377+
}]
2378+
},
23382379
{
23392380
// issue #1259
23402381
code: `
@@ -4728,6 +4769,30 @@ ruleTester.run('prop-types', rule, {
47284769
message: '\'initialValues\' is missing in props validation'
47294770
}]
47304771
},
4772+
{
4773+
// issue #2138
4774+
code: `
4775+
type UsedProps = {|
4776+
usedProp: number,
4777+
|};
4778+
4779+
type UnusedProps = {|
4780+
unusedProp: number,
4781+
|};
4782+
4783+
type Props = {| ...UsedProps, ...UnusedProps |};
4784+
4785+
function MyComponent({ usedProp, notOne }: Props) {
4786+
return <div>{usedProp}</div>;
4787+
}
4788+
`,
4789+
parser: parsers.BABEL_ESLINT,
4790+
errors: [{
4791+
message: "'notOne' is missing in props validation",
4792+
line: 12,
4793+
column: 42
4794+
}]
4795+
},
47314796
{
47324797
// issue #2298
47334798
code: `

0 commit comments

Comments
 (0)