Skip to content

Commit 200351d

Browse files
committed
Add further support for Flow prop types.
This adds support for constructions like this: type Props = {name: string}; class X extends React.Component<void, Props, void> { ... } Which is the preferred way to declare prop types using Flow. The less preferred syntax is still supported: class Z extends React.Component { props: Props; } Fixes jsx-eslint#453
1 parent 7111a70 commit 200351d

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed

lib/rules/prop-types.js

+56
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,45 @@ module.exports = {
131131
return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
132132
}
133133

134+
/**
135+
* Checks whether the given identifier or member expression points to
136+
* `Component`, `React.Component` or `React.PureComponent`.
137+
* Doesn't support aliasing react to anything other than `React`.
138+
* @param {ASTNode} node The AST node being checke.d
139+
* @returns {Boolean} True if the node points to react, otherwise false.
140+
*/
141+
function isReactComponentClass(node) {
142+
if (!node) {
143+
return false;
144+
}
145+
if (node.type === 'Identifier' && node.name === 'Component') {
146+
return true;
147+
} else if (node.type === 'MemberExpression' && node.object.type === 'Identifier') {
148+
return (
149+
node.object.name === 'React' &&
150+
node.property.type === 'Identifier' &&
151+
(node.property.name === 'Component' || node.property.name === 'PureComponent')
152+
);
153+
}
154+
return false;
155+
}
156+
157+
/**
158+
* Checks if we are declaring a class which extends `React.Component`
159+
* with `superTypeParameters`, e.g:
160+
* `class Foo extends React.Component<null, Object, null> {}`.
161+
* @param {ASTNode} node The AST node being checked.
162+
* @returns {Boolean} True if the node is a type annotated props declaration, false if not.
163+
*/
164+
function isAnnotatedClass(node) {
165+
if (node && (node.type === 'ClassExpression' || node.type === 'ClassDeclaration')) {
166+
if (isReactComponentClass(node.superClass) && node.superTypeParameters) {
167+
return true;
168+
}
169+
}
170+
return false;
171+
}
172+
134173
/**
135174
* Checks if we are declaring a `props` class property with a flow type annotation.
136175
* @param {ASTNode} node The AST node being checked.
@@ -818,11 +857,28 @@ module.exports = {
818857
markAnnotatedFunctionArgumentsAsDeclared(node);
819858
}
820859

860+
/**
861+
* Handles classes possibly annotated with Flow.
862+
* @param {ASTNode} node We expect either a ClassDeclaration or ClassExpression.
863+
*/
864+
function handleClass(node) {
865+
if (isAnnotatedClass(node)) {
866+
var typeParameters = node.superTypeParameters.params;
867+
if (typeParameters.length > 1) {
868+
markPropTypesAsDeclared(node, resolveTypeAnnotation(typeParameters[1]));
869+
}
870+
}
871+
}
872+
821873
// --------------------------------------------------------------------------
822874
// Public
823875
// --------------------------------------------------------------------------
824876

825877
return {
878+
ClassDeclaration: handleClass,
879+
880+
ClassExpression: handleClass,
881+
826882
ClassProperty: function(node) {
827883
if (isAnnotatedClassPropsDeclaration(node)) {
828884
markPropTypesAsDeclared(node, resolveTypeAnnotation(node));

tests/lib/rules/prop-types.js

+271
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,142 @@ ruleTester.run('prop-types', rule, {
927927
'}'
928928
].join('\n'),
929929
parser: 'babel-eslint'
930+
}, {
931+
code: [
932+
'class Hello extends React.Component<void, {name: Object}, void> {',
933+
' render () {',
934+
' return <div>Hello {this.props.name.firstname}</div>;',
935+
' }',
936+
'}'
937+
].join('\n'),
938+
parser: 'babel-eslint'
939+
}, {
940+
code: [
941+
'type Props = {name: Object;};',
942+
'class Hello extends React.Component<void, Props, void> {',
943+
' render () {',
944+
' return <div>Hello {this.props.name.firstname}</div>;',
945+
' }',
946+
'}'
947+
].join('\n'),
948+
parser: 'babel-eslint'
949+
}, {
950+
code: [
951+
'import type Props from "fake";',
952+
'class Hello extends React.Component<void, Props, void> {',
953+
' render () {',
954+
' return <div>Hello {this.props.name.firstname}</div>;',
955+
' }',
956+
'}'
957+
].join('\n'),
958+
parser: 'babel-eslint'
959+
}, {
960+
code: [
961+
'class Hello extends React.Component<void, {name: {firstname: string}}, void> {',
962+
' render () {',
963+
' return <div>Hello {this.props.name.firstname}</div>;',
964+
' }',
965+
'}'
966+
].join('\n'),
967+
parser: 'babel-eslint'
968+
}, {
969+
code: [
970+
'type Props = {name: {firstname: string;};};',
971+
'class Hello extends React.Component<void, Props, void> {',
972+
' render () {',
973+
' return <div>Hello {this.props.name.firstname}</div>;',
974+
' }',
975+
'}'
976+
].join('\n'),
977+
parser: 'babel-eslint'
978+
}, {
979+
code: [
980+
'type Props = {name: {firstname: string; lastname: string;};};',
981+
'class Hello extends React.Component<void, Props, void> {',
982+
' render () {',
983+
' return <div>Hello {this.props.name}</div>;',
984+
' }',
985+
'}'
986+
].join('\n'),
987+
parser: 'babel-eslint'
988+
}, {
989+
code: [
990+
'type Person = {name: {firstname: string;}};',
991+
'class Hello extends React.Component<void, {people: Person[]}, void> {',
992+
' render () {',
993+
' var names = [];',
994+
' for (var i = 0; i < this.props.people.length; i++) {',
995+
' names.push(this.props.people[i].name.firstname);',
996+
' }',
997+
' return <div>Hello {names.join(', ')}</div>;',
998+
' }',
999+
'}'
1000+
].join('\n'),
1001+
parser: 'babel-eslint'
1002+
}, {
1003+
code: [
1004+
'type Person = {name: {firstname: string;}};',
1005+
'type Props = {people: Person[];};',
1006+
'class Hello extends React.Component<void, Props, void> {',
1007+
' render () {',
1008+
' var names = [];',
1009+
' for (var i = 0; i < this.props.people.length; i++) {',
1010+
' names.push(this.props.people[i].name.firstname);',
1011+
' }',
1012+
' return <div>Hello {names.join(', ')}</div>;',
1013+
' }',
1014+
'}'
1015+
].join('\n'),
1016+
parser: 'babel-eslint'
1017+
}, {
1018+
code: [
1019+
'type Person = {name: {firstname: string;}};',
1020+
'type Props = {people: Person[]|Person;};',
1021+
'class Hello extends React.Component<void, Props, void> {',
1022+
' render () {',
1023+
' var names = [];',
1024+
' if (Array.isArray(this.props.people)) {',
1025+
' for (var i = 0; i < this.props.people.length; i++) {',
1026+
' names.push(this.props.people[i].name.firstname);',
1027+
' }',
1028+
' } else {',
1029+
' names.push(this.props.people.name.firstname);',
1030+
' }',
1031+
' return <div>Hello {names.join(', ')}</div>;',
1032+
' }',
1033+
'}'
1034+
].join('\n'),
1035+
parser: 'babel-eslint'
1036+
}, {
1037+
code: [
1038+
'type Props = {ok: string | boolean;};',
1039+
'class Hello extends React.Component<void, Props, void> {',
1040+
' render () {',
1041+
' return <div>Hello {this.props.ok}</div>;',
1042+
' }',
1043+
'}'
1044+
].join('\n'),
1045+
parser: 'babel-eslint'
1046+
}, {
1047+
code: [
1048+
'type Props = {result: {ok: string | boolean;}|{ok: number | Array}};',
1049+
'class Hello extends React.Component<void, Props, void> {',
1050+
' render () {',
1051+
' return <div>Hello {this.props.result.ok}</div>;',
1052+
' }',
1053+
'}'
1054+
].join('\n'),
1055+
parser: 'babel-eslint'
1056+
}, {
1057+
code: [
1058+
'type Props = {result?: {ok?: ?string | boolean;}|{ok?: ?number | Array}};',
1059+
'class Hello extends React.Component<void, Props, void> {',
1060+
' render () {',
1061+
' return <div>Hello {this.props.result.ok}</div>;',
1062+
' }',
1063+
'}'
1064+
].join('\n'),
1065+
parser: 'babel-eslint'
9301066
}, {
9311067
code: [
9321068
'class Hello extends React.Component {',
@@ -2110,6 +2246,141 @@ ruleTester.run('prop-types', rule, {
21102246
errors: [
21112247
{message: '\'name\' is missing in props validation'}
21122248
]
2249+
}, {
2250+
code: [
2251+
'class Hello extends React.Component<void, {name: Object}, void> {',
2252+
' render () {',
2253+
' return <div>Hello {this.props.firstname}</div>;',
2254+
' }',
2255+
'}'
2256+
].join('\n'),
2257+
parser: 'babel-eslint',
2258+
errors: [
2259+
{message: '\'firstname\' is missing in props validation'}
2260+
]
2261+
}, {
2262+
code: [
2263+
'type Props = {name: Object;};',
2264+
'class Hello extends React.Component<void, Props, void> {',
2265+
' render () {',
2266+
' return <div>Hello {this.props.firstname}</div>;',
2267+
' }',
2268+
'}'
2269+
].join('\n'),
2270+
parser: 'babel-eslint',
2271+
errors: [
2272+
{message: '\'firstname\' is missing in props validation'}
2273+
]
2274+
}, {
2275+
code: [
2276+
'class Hello extends React.Component<void, {name: {firstname: string}}, void> {',
2277+
' render () {',
2278+
' return <div>Hello {this.props.name.lastname}</div>;',
2279+
' }',
2280+
'}'
2281+
].join('\n'),
2282+
parser: 'babel-eslint',
2283+
errors: [
2284+
{message: '\'name.lastname\' is missing in props validation'}
2285+
]
2286+
}, {
2287+
code: [
2288+
'type Props = {name: {firstname: string;};};',
2289+
'class Hello extends React.Component<void, Props, void> {',
2290+
' render () {',
2291+
' return <div>Hello {this.props.name.lastname}</div>;',
2292+
' }',
2293+
'}'
2294+
].join('\n'),
2295+
parser: 'babel-eslint',
2296+
errors: [
2297+
{message: '\'name.lastname\' is missing in props validation'}
2298+
]
2299+
}, {
2300+
code: [
2301+
'class Hello extends React.Component<void, {person: {name: {firstname: string}}}, void> {',
2302+
' render () {',
2303+
' return <div>Hello {this.props.person.name.lastname}</div>;',
2304+
' }',
2305+
'}'
2306+
].join('\n'),
2307+
parser: 'babel-eslint',
2308+
errors: [
2309+
{message: '\'person.name.lastname\' is missing in props validation'}
2310+
]
2311+
}, {
2312+
code: [
2313+
'type Props = {person: {name: {firstname: string;};};};',
2314+
'class Hello extends React.Component<void, Props, void> {',
2315+
' render () {',
2316+
' return <div>Hello {this.props.person.name.lastname}</div>;',
2317+
' }',
2318+
'}'
2319+
].join('\n'),
2320+
parser: 'babel-eslint',
2321+
errors: [
2322+
{message: '\'person.name.lastname\' is missing in props validation'}
2323+
]
2324+
}, {
2325+
code: [
2326+
'type Person = {name: {firstname: string;}};',
2327+
'class Hello extends React.Component<void, {people: Person[]}, void> {',
2328+
' render () {',
2329+
' var names = [];',
2330+
' for (var i = 0; i < this.props.people.length; i++) {',
2331+
' names.push(this.props.people[i].name.lastname);',
2332+
' }',
2333+
' return <div>Hello {names.join(', ')}</div>;',
2334+
' }',
2335+
'}'
2336+
].join('\n'),
2337+
parser: 'babel-eslint',
2338+
errors: [
2339+
{message: '\'people[].name.lastname\' is missing in props validation'}
2340+
]
2341+
}, {
2342+
code: [
2343+
'type Person = {name: {firstname: string;}};',
2344+
'type Props = {people: Person[];};',
2345+
'class Hello extends React.Component<void, Props, void> {',
2346+
' render () {',
2347+
' var names = [];',
2348+
' for (var i = 0; i < this.props.people.length; i++) {',
2349+
' names.push(this.props.people[i].name.lastname);',
2350+
' }',
2351+
' return <div>Hello {names.join(', ')}</div>;',
2352+
' }',
2353+
'}'
2354+
].join('\n'),
2355+
parser: 'babel-eslint',
2356+
errors: [
2357+
{message: '\'people[].name.lastname\' is missing in props validation'}
2358+
]
2359+
}, {
2360+
code: [
2361+
'type Props = {result?: {ok: string | boolean;}|{ok: number | Array}};',
2362+
'class Hello extends React.Component<void, Props, void> {',
2363+
' render () {',
2364+
' return <div>Hello {this.props.result.notok}</div>;',
2365+
' }',
2366+
'}'
2367+
].join('\n'),
2368+
parser: 'babel-eslint',
2369+
errors: [
2370+
{message: '\'result.notok\' is missing in props validation'}
2371+
]
2372+
}, {
2373+
code: [
2374+
'class Hello extends React.Component<void, {}, void> {',
2375+
' render () {',
2376+
' return <div>Hello {this.props.name}</div>;',
2377+
' }',
2378+
'}'
2379+
].join('\n'),
2380+
parser: 'babel-eslint',
2381+
errors: [
2382+
{message: '\'name\' is missing in props validation'}
2383+
]
21132384
}, {
21142385
code: [
21152386
'class Hello extends React.Component {',

0 commit comments

Comments
 (0)