Skip to content

Commit f0b2602

Browse files
authored
Merge pull request #1404 from jseminck/prop-types-union
Add support for Flow IntersectionTypeAnnotation to prop-types and no-unused-prop-types
2 parents d194186 + e2bcba5 commit f0b2602

File tree

6 files changed

+173
-6
lines changed

6 files changed

+173
-6
lines changed

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,46 @@ module.exports = {
163163
}));
164164
}
165165

166+
/**
167+
* Handles Props defined in IntersectionTypeAnnotation nodes
168+
* e.g. type Props = PropsA & PropsB
169+
* @param {ASTNode} intersectionTypeAnnotation ObjectExpression node.
170+
* @returns {Object[]}
171+
*/
172+
function getPropertiesFromIntersectionTypeAnnotationNode(annotation) {
173+
return annotation.types.reduce((properties, type) => {
174+
annotation = resolveGenericTypeAnnotation(type);
175+
176+
if (annotation && annotation.id) {
177+
annotation = findVariableByName(annotation.id.name);
178+
}
179+
180+
return properties.concat(annotation.properties);
181+
}, []);
182+
}
183+
166184
/**
167185
* Extracts a PropType from a TypeAnnotation node.
168186
* @param {ASTNode} node TypeAnnotation node.
169187
* @returns {Object[]} Array of PropType object representations, to be consumed by `addPropTypesToComponent`.
170188
*/
171189
function getPropTypesFromTypeAnnotation(node) {
172-
let properties;
190+
let properties = [];
173191

174192
switch (node.typeAnnotation.type) {
175193
case 'GenericTypeAnnotation':
176194
let annotation = resolveGenericTypeAnnotation(node.typeAnnotation);
177195

178-
if (annotation && annotation.id) {
179-
annotation = findVariableByName(annotation.id.name);
196+
if (annotation && annotation.type === 'IntersectionTypeAnnotation') {
197+
properties = getPropertiesFromIntersectionTypeAnnotationNode(annotation);
198+
} else {
199+
if (annotation && annotation.id) {
200+
annotation = findVariableByName(annotation.id.name);
201+
}
202+
203+
properties = annotation ? (annotation.properties || []) : [];
180204
}
181205

182-
properties = annotation ? (annotation.properties || []) : [];
183206
break;
184207

185208
case 'UnionTypeAnnotation':
@@ -314,7 +337,6 @@ module.exports = {
314337
if (!component) {
315338
return;
316339
}
317-
318340
addPropTypesToComponent(component, getPropTypesFromTypeAnnotation(node.typeAnnotation, context));
319341
}
320342

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,26 @@ module.exports = {
736736
return;
737737
}
738738
break;
739+
case 'IntersectionTypeAnnotation':
740+
propTypes.types.forEach(annotation => {
741+
const propsType = typeScope(annotation.id.name);
742+
iterateProperties(propsType.properties, (key, value) => {
743+
if (!value) {
744+
ignorePropsValidation = true;
745+
return;
746+
}
747+
748+
let types = buildTypeAnnotationDeclarationTypes(value, key);
749+
if (types === true) {
750+
types = {};
751+
}
752+
types.fullName = key;
753+
types.name = key;
754+
types.node = value;
755+
declaredPropTypes.push(types);
756+
});
757+
});
758+
break;
739759
case null:
740760
break;
741761
default:

lib/rules/prop-types.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const Components = require('../util/Components');
1212
const variable = require('../util/variable');
1313
const annotations = require('../util/annotations');
1414
const versionUtil = require('../util/version');
15-
1615
// ------------------------------------------------------------------------------
1716
// Constants
1817
// ------------------------------------------------------------------------------
@@ -794,6 +793,18 @@ module.exports = {
794793
return;
795794
}
796795
break;
796+
case 'IntersectionTypeAnnotation':
797+
propTypes.types.forEach(annotation => {
798+
const propsType = typeScope(annotation.id.name);
799+
iterateProperties(propsType.properties, (key, value) => {
800+
if (!value) {
801+
ignorePropsValidation = true;
802+
return;
803+
}
804+
declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value);
805+
});
806+
});
807+
break;
797808
case null:
798809
break;
799810
default:

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,25 @@ ruleTester.run('default-props-match-prop-types', rule, {
682682
].join('\n'),
683683
parser: 'babel-eslint'
684684
},
685+
{
686+
code: `
687+
type PropsA = { foo?: string };
688+
type PropsB = { bar?: string, fooBar: string };
689+
type Props = PropsA & PropsB;
690+
691+
class Bar extends React.Component {
692+
props: Props;
693+
static defaultProps = {
694+
foo: "foo",
695+
}
696+
697+
render() {
698+
return <div>{this.props.foo} - {this.props.bar}</div>
699+
}
700+
}
701+
`,
702+
parser: 'babel-eslint'
703+
},
685704
{
686705
code: [
687706
'import type Props from "fake";',
@@ -1509,6 +1528,34 @@ ruleTester.run('default-props-match-prop-types', rule, {
15091528
column: 36
15101529
}
15111530
]
1531+
},
1532+
{
1533+
code: `
1534+
type PropsA = { foo: string };
1535+
type PropsB = { bar: string };
1536+
type Props = PropsA & PropsB;
1537+
1538+
class Bar extends React.Component {
1539+
props: Props;
1540+
static defaultProps = {
1541+
fooBar: "fooBar",
1542+
foo: "foo",
1543+
}
1544+
1545+
render() {
1546+
return <div>{this.props.foo} - {this.props.bar}</div>
1547+
}
1548+
}
1549+
`,
1550+
parser: 'babel-eslint',
1551+
errors: [
1552+
{
1553+
message: 'defaultProp "fooBar" has no corresponding propTypes declaration.'
1554+
},
1555+
{
1556+
message: 'defaultProp "foo" defined for isRequired propType.'
1557+
}
1558+
]
15121559
}
15131560
]
15141561
});

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,21 @@ ruleTester.run('no-unused-prop-types', rule, {
824824
'}'
825825
].join('\n'),
826826
parser: 'babel-eslint'
827+
}, {
828+
code: `
829+
type PropsA = { a: string }
830+
type PropsB = { b: string }
831+
type Props = PropsA & PropsB;
832+
833+
class MyComponent extends React.Component {
834+
props: Props;
835+
836+
render() {
837+
return <div>{this.props.a} - {this.props.b}</div>
838+
}
839+
}
840+
`,
841+
parser: 'babel-eslint'
827842
}, {
828843
code: [
829844
'import type Props from "fake";',
@@ -2637,6 +2652,24 @@ ruleTester.run('no-unused-prop-types', rule, {
26372652
errors: [
26382653
{message: '\'unused\' PropType is defined but prop is never used'}
26392654
]
2655+
}, {
2656+
code: `
2657+
type PropsA = { a: string }
2658+
type PropsB = { b: string }
2659+
type Props = PropsA & PropsB;
2660+
2661+
class MyComponent extends React.Component {
2662+
props: Props;
2663+
2664+
render() {
2665+
return <div>{this.props.a}</div>
2666+
}
2667+
}
2668+
`,
2669+
parser: 'babel-eslint',
2670+
errors: [
2671+
{message: '\'b\' PropType is defined but prop is never used'}
2672+
]
26402673
}, {
26412674
code: [
26422675
'class Hello extends React.Component {',

tests/lib/rules/prop-types.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,7 +1669,23 @@ ruleTester.run('prop-types', rule, {
16691669
`,
16701670
settings: {react: {flowVersion: '0.53'}},
16711671
parser: 'babel-eslint'
1672+
}, {
1673+
code: `
1674+
type PropsA = { foo: string };
1675+
type PropsB = { bar: string };
1676+
type Props = PropsA & PropsB;
1677+
1678+
class Bar extends React.Component {
1679+
props: Props;
1680+
1681+
render() {
1682+
return <div>{this.props.foo} - {this.props.bar}</div>
1683+
}
1684+
}
1685+
`,
1686+
parser: 'babel-eslint'
16721687
},
1688+
16731689
// issue #1288
16741690
`function Foo() {
16751691
const props = {}
@@ -3256,6 +3272,24 @@ ruleTester.run('prop-types', rule, {
32563272
type: 'Identifier'
32573273
}],
32583274
parser: 'babel-eslint'
3275+
}, {
3276+
code: `
3277+
type PropsA = {foo: string };
3278+
type PropsB = { bar: string };
3279+
type Props = PropsA & PropsB;
3280+
3281+
class MyComponent extends React.Component {
3282+
props: Props;
3283+
3284+
render() {
3285+
return <div>{this.props.foo} - {this.props.bar} - {this.props.fooBar}</div>
3286+
}
3287+
}
3288+
`,
3289+
parser: 'babel-eslint',
3290+
errors: [{
3291+
message: '\'fooBar\' is missing in props validation'
3292+
}]
32593293
}
32603294
]
32613295
});

0 commit comments

Comments
 (0)