Skip to content

Commit 5f7b1b2

Browse files
author
Keyan Zhang
committed
WIP flow transformation; switched parser to Flow
1 parent 84f8b71 commit 5f7b1b2

File tree

8 files changed

+288
-16
lines changed

8 files changed

+288
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"lint": "eslint ."
1010
},
1111
"dependencies": {
12-
"jscodeshift": "^0.3.24",
12+
"jscodeshift": "^0.3.25",
1313
"babel-eslint": "^6.0.5",
1414
"babel-plugin-transform-object-rest-spread": "^6.6.5",
1515
"babel-preset-es2015": "^6.6.0",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* @flow */
2+
3+
var React = require('react');
4+
5+
var Component = React.createClass({
6+
propTypes: {
7+
optionalArray: React.PropTypes.array,
8+
optionalBool: React.PropTypes.bool,
9+
optionalFunc: React.PropTypes.func,
10+
optionalNumber: React.PropTypes.number,
11+
optionalObject: React.PropTypes.object,
12+
optionalString: React.PropTypes.string,
13+
optionalNode: React.PropTypes.node,
14+
optionalElement: React.PropTypes.element,
15+
optionalMessage: React.PropTypes.instanceOf(Message),
16+
optionalEnum: React.PropTypes.oneOf(['News', 'Photos', 1, true, null]),
17+
optionalUnion: React.PropTypes.oneOfType([
18+
React.PropTypes.string,
19+
React.PropTypes.number,
20+
React.PropTypes.instanceOf(Message),
21+
]),
22+
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
23+
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
24+
optionalObjectWithShape: React.PropTypes.shape({
25+
color: React.PropTypes.string,
26+
fontSize: React.PropTypes.number,
27+
}),
28+
requiredFunc: React.PropTypes.func.isRequired,
29+
requiredAny: React.PropTypes.any.isRequired,
30+
},
31+
32+
render: function() {
33+
return (
34+
<div>type safety</div>
35+
);
36+
},
37+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* @flow */
2+
3+
var React = require('react');
4+
5+
class Component extends React.Component {
6+
props: {
7+
optionalArray?: Array<any>,
8+
optionalBool?: boolean,
9+
optionalFunc?: Function,
10+
optionalNumber?: number,
11+
optionalObject?: Object,
12+
optionalString?: string,
13+
optionalNode?: any,
14+
optionalElement?: any,
15+
optionalMessage?: Message,
16+
optionalEnum?: 'News' | 'Photos' | 1 | true | null,
17+
optionalUnion?: string | number | Message,
18+
optionalArrayOf?: Array<number>,
19+
optionalObjectOf?: {[key: string]: number},
20+
optionalObjectWithShape?: {color: string, fontSize: number},
21+
requiredFunc: Function,
22+
requiredAny: any,
23+
};
24+
25+
static propTypes = {
26+
optionalArray: React.PropTypes.array,
27+
optionalBool: React.PropTypes.bool,
28+
optionalFunc: React.PropTypes.func,
29+
optionalNumber: React.PropTypes.number,
30+
optionalObject: React.PropTypes.object,
31+
optionalString: React.PropTypes.string,
32+
optionalNode: React.PropTypes.node,
33+
optionalElement: React.PropTypes.element,
34+
optionalMessage: React.PropTypes.instanceOf(Message),
35+
optionalEnum: React.PropTypes.oneOf(['News', 'Photos', 1, true, null]),
36+
optionalUnion: React.PropTypes.oneOfType([
37+
React.PropTypes.string,
38+
React.PropTypes.number,
39+
React.PropTypes.instanceOf(Message),
40+
]),
41+
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
42+
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
43+
optionalObjectWithShape: React.PropTypes.shape({
44+
color: React.PropTypes.string,
45+
fontSize: React.PropTypes.number,
46+
}),
47+
requiredFunc: React.PropTypes.func.isRequired,
48+
requiredAny: React.PropTypes.any.isRequired,
49+
};
50+
51+
render() {
52+
return (
53+
<div>type safety</div>
54+
);
55+
}
56+
}

transforms/__testfixtures__/class.input.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var MyComponent2 = React.createClass({
1111
return {a: 1};
1212
},
1313
foo: function(): void {
14+
const x = (a: Object, b: string): void => {}; // This code cannot be parsed by Babel v5
1415
pass(this.foo);
1516
this.forceUpdate();
1617
},

transforms/__testfixtures__/class.output.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class MyComponent2 extends React.Component {
1010
static defaultProps = {a: 1};
1111

1212
foo = (): void => {
13+
const x = (a: Object, b: string): void => {}; // This code cannot be parsed by Babel v5
1314
pass(this.foo);
1415
this.forceUpdate();
1516
};

transforms/__tests__/class-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class-test2');
2121
defineTest(__dirname, 'class', null, 'export-default-class');
2222
defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class-pure-mixin1');
2323
defineTest(__dirname, 'class', null, 'class-pure-mixin2');
24-
defineTest(__dirname, 'class', null, 'class-property-field');
2524
defineTest(__dirname, 'class', null, 'class-initial-state');
25+
defineTest(__dirname, 'class', null, 'class-property-field');
26+
defineTest(__dirname, 'class', { flow: true }, 'class-proptypes');

transforms/class.js

Lines changed: 188 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ module.exports = (file, api, options) => {
9393
!filterDefaultPropsField(prop) &&
9494
!filterGetInitialStateField(prop) &&
9595
!isFunctionExpression(prop) &&
96-
!isPrimExpression(prop) &&
96+
!isPrimProperty(prop) &&
9797
MIXIN_KEY != prop.key.name
9898
)
9999
));
@@ -154,16 +154,19 @@ module.exports = (file, api, options) => {
154154
node.value.type === 'FunctionExpression'
155155
);
156156

157-
const isPrimExpression = node => (
158-
node.key &&
159-
node.key.type === 'Identifier' &&
160-
node.value && (
161-
node.value.type === 'Literal' || ( // TODO this might change in babylon v6
162-
node.value.type === 'Identifier' &&
163-
node.value.name === 'undefined'
164-
))
157+
const isPrimProperty = prop => (
158+
prop.key &&
159+
prop.key.type === 'Identifier' &&
160+
prop.value &&
161+
isPrimExpression(prop.value)
165162
);
166163

164+
const isPrimExpression = node => (
165+
node.type === 'Literal' || ( // TODO this might change in babylon v6
166+
node.type === 'Identifier' &&
167+
node.name === 'undefined'
168+
));
169+
167170
// Collects `childContextTypes`, `contextTypes`, `displayName`, and `propTypes`;
168171
// simplifies `getDefaultProps` or converts it to an IIFE;
169172
// and collects everything else in the `statics` property object.
@@ -188,7 +191,7 @@ module.exports = (file, api, options) => {
188191
.filter(prop =>
189192
!(filterDefaultPropsField(prop) || filterGetInitialStateField(prop))
190193
)
191-
.filter(prop => isFunctionExpression(prop) || isPrimExpression(prop));
194+
.filter(prop => isFunctionExpression(prop) || isPrimProperty(prop));
192195

193196
const findRequirePathAndBinding = (moduleName) => {
194197
let result = null;
@@ -211,7 +214,7 @@ module.exports = (file, api, options) => {
211214
importStatement.forEach(path => {
212215
result = {
213216
path,
214-
binding: path.value.specifiers[0].local.name,
217+
binding: path.value.specifiers[0].id.name,
215218
};
216219
});
217220
} else if (requireStatement.size()) {
@@ -391,6 +394,169 @@ module.exports = (file, api, options) => {
391394
false
392395
), prop);
393396

397+
// ---------------------------------------------------------------------------
398+
// Flow!
399+
400+
const flowAnyType = j.anyTypeAnnotation();
401+
402+
const literalToFlowType = node => {
403+
switch (typeof node.value) {
404+
case 'string':
405+
return j.stringLiteralTypeAnnotation(node.value, node.raw);
406+
case 'number':
407+
return j.numberLiteralTypeAnnotation(node.value, node.raw);
408+
case 'boolean':
409+
return j.booleanLiteralTypeAnnotation(node.value, node.raw);
410+
case 'object':
411+
return j.nullLiteralTypeAnnotation();
412+
default:
413+
return flowAnyType; // meh
414+
}
415+
};
416+
417+
const propTypeToFlowMapping = {
418+
// prim types
419+
any: flowAnyType,
420+
array: j.genericTypeAnnotation(
421+
j.identifier('Array'),
422+
j.typeParameterInstantiation([flowAnyType])
423+
),
424+
bool: j.booleanTypeAnnotation(),
425+
element: flowAnyType,
426+
func: j.genericTypeAnnotation(
427+
j.identifier('Function'),
428+
null
429+
),
430+
node: flowAnyType,
431+
number: j.numberTypeAnnotation(),
432+
object: j.genericTypeAnnotation(
433+
j.identifier('Object'),
434+
null
435+
),
436+
string: j.stringTypeAnnotation(),
437+
438+
// type classes
439+
arrayOf: (type) => j.genericTypeAnnotation(
440+
j.identifier('Array'),
441+
j.typeParameterInstantiation([type])
442+
),
443+
instanceOf: (type) => j.genericTypeAnnotation(
444+
type,
445+
null
446+
),
447+
objectOf: (type) => j.objectTypeAnnotation(
448+
[],
449+
[j.objectTypeIndexer(j.identifier('key'), j.stringTypeAnnotation(), type)],
450+
[]
451+
),
452+
oneOf: (typeList) => j.unionTypeAnnotation(typeList),
453+
oneOfType: (typeList) => j.unionTypeAnnotation(typeList),
454+
};
455+
456+
const propTypeToFlowAnnotation = val => {
457+
let cursor = val;
458+
let isOptional = true;
459+
let typeResult = flowAnyType;
460+
461+
if (
462+
cursor.type === 'MemberExpression' &&
463+
cursor.property.type === 'Identifier' &&
464+
cursor.property.name === 'isRequired'
465+
) {
466+
isOptional = false;
467+
cursor = cursor.object;
468+
}
469+
470+
if (
471+
cursor.type === 'CallExpression'
472+
) {
473+
switch (cursor.callee.property.name) {
474+
case 'arrayOf': {
475+
const arg = cursor.arguments[0];
476+
const constructor = propTypeToFlowMapping['arrayOf'];
477+
typeResult = constructor(
478+
propTypeToFlowAnnotation(arg)[0]
479+
);
480+
break;
481+
}
482+
case 'instanceOf': {
483+
const arg = cursor.arguments[0];
484+
if (arg.type !== 'Identifier') {
485+
typeResult = flowAnyType;
486+
break;
487+
}
488+
489+
const constructor = propTypeToFlowMapping['instanceOf'];
490+
typeResult = constructor(arg);
491+
break;
492+
}
493+
case 'objectOf': {
494+
const arg = cursor.arguments[0];
495+
const constructor = propTypeToFlowMapping['objectOf'];
496+
typeResult = constructor(
497+
propTypeToFlowAnnotation(arg)[0]
498+
);
499+
break;
500+
}
501+
case 'oneOf': {
502+
const argList = cursor.arguments[0].elements;
503+
if (!argList.every(node => node.type === 'Literal')) {
504+
typeResult = flowAnyType;
505+
break;
506+
}
507+
const constructor = propTypeToFlowMapping['oneOf'];
508+
typeResult = constructor(
509+
argList.map(literalToFlowType)
510+
);
511+
break;
512+
}
513+
case 'oneOfType': {
514+
const argList = cursor.arguments[0].elements;
515+
const constructor = propTypeToFlowMapping['oneOfType'];
516+
typeResult = constructor(
517+
argList.map(arg => propTypeToFlowAnnotation(arg)[0])
518+
);
519+
break;
520+
}
521+
case 'shape': { // TODO
522+
523+
}
524+
}
525+
} else if (
526+
cursor.type === 'MemberExpression' &&
527+
cursor.property.type === 'Identifier'
528+
) {
529+
typeResult = propTypeToFlowMapping[cursor.property.name] || flowAnyType;
530+
}
531+
532+
return [typeResult, isOptional];
533+
};
534+
535+
const createFlowAnnotationsFromPropTypesProperties = (prop) => {
536+
const typeProperty = [];
537+
538+
if (!prop) {
539+
return typeProperty;
540+
}
541+
542+
prop.value.properties.forEach(typeProp => {
543+
const name = typeProp.key.name;
544+
const [valueType, isOptional] = propTypeToFlowAnnotation(typeProp.value);
545+
typeProperty.push(j.objectTypeProperty(
546+
j.identifier(name),
547+
valueType,
548+
isOptional
549+
));
550+
});
551+
552+
return j.classProperty(
553+
j.identifier('props'),
554+
null,
555+
j.typeAnnotation(j.objectTypeAnnotation(typeProperty)),
556+
false
557+
);
558+
};
559+
394560
// if there's no `getInitialState` or the `getInitialState` function is simple
395561
// (i.e., it's just a return statement) then we don't need a constructor.
396562
// we can simply lift `state = {...}` as a property initializer.
@@ -428,7 +594,7 @@ module.exports = (file, api, options) => {
428594
}
429595

430596
const propertiesAndMethods = rawProperties.map(prop => {
431-
if (isPrimExpression(prop)) {
597+
if (isPrimProperty(prop)) {
432598
return createClassProperty(prop);
433599
} else if (AUTOBIND_IGNORE_KEYS[prop.key.name]) {
434600
return createMethodDefinition(prop);
@@ -437,10 +603,17 @@ module.exports = (file, api, options) => {
437603
return createArrowProperty(prop);
438604
});
439605

606+
const flowPropsAnnotation = options['flow'] ?
607+
createFlowAnnotationsFromPropTypesProperties(
608+
staticProperties.find((path) => path.key.name === 'propTypes')
609+
) :
610+
[];
611+
440612
return withComments(j.classDeclaration(
441613
name ? j.identifier(name) : null,
442614
j.classBody(
443615
[].concat(
616+
flowPropsAnnotation,
444617
staticProperties,
445618
maybeConstructor,
446619
initialStateProperty,
@@ -516,7 +689,7 @@ module.exports = (file, api, options) => {
516689
// Ignore import bindings
517690
.filter(identifierPath => !(
518691
path.value.type === 'ImportDeclaration' &&
519-
path.value.specifiers.some(specifier => specifier.local === identifierPath.value)
692+
path.value.specifiers.some(specifier => specifier.id === identifierPath.value)
520693
))
521694
// Ignore properties in MemberExpressions
522695
.filter(identifierPath => {
@@ -618,3 +791,5 @@ module.exports = (file, api, options) => {
618791

619792
return null;
620793
};
794+
795+
module.exports.parser = 'flow';

0 commit comments

Comments
 (0)