Skip to content

Instantly share code, notes, and snippets.

@dounan
Last active May 24, 2017 23:59
Show Gist options
  • Save dounan/ce151dedde9aa590447a80a0a9321060 to your computer and use it in GitHub Desktop.
Save dounan/ce151dedde9aa590447a80a0a9321060 to your computer and use it in GitHub Desktop.
A modified version of React's class.js codemod that does not use the create-react-class module.
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
// jscodeshift -t ./transforms/careful-class.js --mixin-module-name=react-addons-pure-render-mixin --flow=true --pure-component=true --remove-runtime-proptypes=false --display-name=true --explicit-require=true --extensions=jsx ../flexport/webpack/assets/javascripts
'use strict';
const { basename, extname, dirname } = require('path');
module.exports = (file, api, options) => {
const j = api.jscodeshift;
require('./utils/array-polyfills');
const ReactUtils = require('./utils/ReactUtils')(j);
const printOptions =
options.printOptions || {
quote: 'single',
trailingComma: true,
flowObjectCommas: true,
arrowParensAlways: true,
arrayBracketSpacing: false,
objectCurlySpacing: false,
};
const root = j(file.source);
// retain top comments
const { comments: topComments } = root.find(j.Program).get('body', 0).node;
const AUTOBIND_IGNORE_KEYS = {
componentDidMount: true,
componentDidUpdate: true,
componentWillReceiveProps: true,
componentWillMount: true,
componentWillUpdate: true,
componentWillUnmount: true,
getChildContext: true,
getDefaultProps: true,
getInitialState: true,
render: true,
shouldComponentUpdate: true,
};
const DEFAULT_PROPS_FIELD = 'getDefaultProps';
const DEFAULT_PROPS_KEY = 'defaultProps';
const GET_INITIAL_STATE_FIELD = 'getInitialState';
const DEPRECATED_APIS = [
'getDOMNode',
'isMounted',
'replaceProps',
'replaceState',
'setProps',
];
const PURE_MIXIN_MODULE_NAME = options['mixin-module-name'] ||
'react-addons-pure-render-mixin';
const CREATE_CLASS_MODULE_NAME = options['create-class-module-name'] ||
'create-react-class';
const CREATE_CLASS_VARIABLE_NAME = options['create-class-variable-name'] ||
'createReactClass';
const STATIC_KEY = 'statics';
const STATIC_KEYS = {
childContextTypes: true,
contextTypes: true,
displayName: true,
propTypes: true,
};
const MIXIN_KEY = 'mixins';
const NO_CONVERSION = options.conversion === false;
const NO_DISPLAY_NAME = options['display-name'] === false;
let shouldTransformFlow = false;
if (options['flow']) {
const programBodyNode = root.find(j.Program).get('body', 0).node;
if (programBodyNode && programBodyNode.comments) {
programBodyNode.comments.forEach(node => {
if (node.value.indexOf('@flow') !== -1) {
shouldTransformFlow = true;
}
});
}
}
// ---------------------------------------------------------------------------
// Helpers
const createFindPropFn = prop => property => (
property.key &&
property.key.type === 'Identifier' &&
property.key.name === prop
);
const filterDefaultPropsField = node =>
createFindPropFn(DEFAULT_PROPS_FIELD)(node);
const filterGetInitialStateField = node =>
createFindPropFn(GET_INITIAL_STATE_FIELD)(node);
const findGetInitialState = specPath =>
specPath.properties.find(createFindPropFn(GET_INITIAL_STATE_FIELD));
const withComments = (to, from) => {
to.comments = from.comments;
return to;
};
const isPrimExpression = node => (
node.type === 'Literal' || ( // NOTE this might change in babylon v6
node.type === 'Identifier' &&
node.name === 'undefined'
));
const isFunctionExpression = node => (
node.key &&
node.key.type === 'Identifier' &&
node.value &&
node.value.type === 'FunctionExpression'
);
const isPrimProperty = prop => (
prop.key &&
prop.key.type === 'Identifier' &&
prop.value &&
isPrimExpression(prop.value)
);
const isPrimPropertyWithTypeAnnotation = prop => (
prop.key &&
prop.key.type === 'Identifier' &&
prop.value &&
prop.value.type === 'TypeCastExpression' &&
isPrimExpression(prop.value.expression)
);
const hasSingleReturnStatement = value => (
value.type === 'FunctionExpression' &&
value.body &&
value.body.type === 'BlockStatement' &&
value.body.body &&
value.body.body.length === 1 &&
value.body.body[0].type === 'ReturnStatement' &&
value.body.body[0].argument
);
const isInitialStateLiftable = getInitialState => {
if (!getInitialState || !(getInitialState.value)) {
return true;
}
return hasSingleReturnStatement(getInitialState.value);
};
// ---------------------------------------------------------------------------
// Checks if the module uses mixins or accesses deprecated APIs.
const checkDeprecatedAPICalls = classPath =>
DEPRECATED_APIS.reduce(
(acc, name) =>
acc + j(classPath)
.find(j.Identifier, {name})
.size(),
0
) > 0;
const hasNoCallsToDeprecatedAPIs = classPath => {
if (checkDeprecatedAPICalls(classPath)) {
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because of deprecated API calls. Remove calls to ' +
DEPRECATED_APIS.join(', ') + ' in your React component and re-run ' +
'this script.'
);
return false;
}
return true;
};
const hasNoRefsToAPIsThatWillBeRemoved = classPath => {
const hasInvalidCalls = (
j(classPath).find(j.MemberExpression, {
object: {type: 'ThisExpression'},
property: {name: DEFAULT_PROPS_FIELD},
}).size() > 0 ||
j(classPath).find(j.MemberExpression, {
object: {type: 'ThisExpression'},
property: {name: GET_INITIAL_STATE_FIELD},
}).size() > 0
);
if (hasInvalidCalls) {
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because of API calls that will be removed. Remove calls to `' +
DEFAULT_PROPS_FIELD + '` and/or `' + GET_INITIAL_STATE_FIELD +
'` in your React component and re-run this script.'
);
return false;
}
return true;
};
const doesNotUseArguments = classPath => {
const hasArguments = (
j(classPath).find(j.Identifier, {name: 'arguments'}).size() > 0
);
if (hasArguments) {
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because `arguments` was found in your functions. ' +
'Arrow functions do not expose an `arguments` object; ' +
'consider changing to use ES6 spread operator and re-run this script.'
);
return false;
}
return true;
};
const isGetInitialStateConstructorSafe = getInitialState => {
if (!getInitialState) {
return true;
}
const collection = j(getInitialState);
let result = true;
const propsVarDeclarationCount = collection.find(j.VariableDeclarator, {
id: {name: 'props'},
}).size();
const contextVarDeclarationCount = collection.find(j.VariableDeclarator, {
id: {name: 'context'},
}).size();
if (
propsVarDeclarationCount &&
propsVarDeclarationCount !== collection.find(j.VariableDeclarator, {
id: {name: 'props'},
init: {
type: 'MemberExpression',
object: {type: 'ThisExpression'},
property: {name: 'props'},
}
}).size()
) {
result = false;
}
if (
contextVarDeclarationCount &&
contextVarDeclarationCount !== collection.find(j.VariableDeclarator, {
id: {name: 'context'},
init: {
type: 'MemberExpression',
object: {type: 'ThisExpression'},
property: {name: 'context'},
}
}).size()
) {
result = false;
}
return result;
};
const isInitialStateConvertible = classPath => {
const specPath = ReactUtils.directlyGetCreateClassSpec(classPath);
if (!specPath) {
return false;
}
const result = isGetInitialStateConstructorSafe(findGetInitialState(specPath));
if (!result) {
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because of potential shadowing issues were found in ' +
'the React component. Rename variable declarations of `props` and/or `context` ' +
'in your `getInitialState` and re-run this script.'
);
}
return result;
};
const canConvertToClass = classPath => {
const specPath = ReactUtils.directlyGetCreateClassSpec(classPath);
if (!specPath) {
return false;
}
const invalidProperties = specPath.properties.filter(prop => (
!prop.key.name || (
!STATIC_KEYS.hasOwnProperty(prop.key.name) &&
STATIC_KEY != prop.key.name &&
!filterDefaultPropsField(prop) &&
!filterGetInitialStateField(prop) &&
!isFunctionExpression(prop) &&
!isPrimProperty(prop) &&
!isPrimPropertyWithTypeAnnotation(prop) &&
MIXIN_KEY != prop.key.name
)
));
if (invalidProperties.length) {
const invalidText = invalidProperties
.map(prop => prop.key.name ? prop.key.name : prop.key)
.join(', ');
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because of invalid field(s) `' + invalidText + '` on ' +
'the React component. Remove any right-hand-side expressions that ' +
'are not simple, like: `componentWillUpdate: createWillUpdate()` or ' +
'`render: foo ? renderA : renderB`.'
);
}
return !invalidProperties.length;
};
const areMixinsConvertible = (mixinIdentifierNames, classPath) => {
if (
ReactUtils.directlyHasMixinsField(classPath) &&
!ReactUtils.directlyHasSpecificMixins(classPath, mixinIdentifierNames)
) {
return false;
}
return true;
};
// ---------------------------------------------------------------------------
// Collectors
const pickReturnValueOrCreateIIFE = value => {
if (hasSingleReturnStatement(value)) {
return value.body.body[0].argument;
} else {
return j.callExpression(
value,
[]
);
}
};
const createDefaultProps = prop =>
withComments(
j.property(
'init',
j.identifier(DEFAULT_PROPS_KEY),
pickReturnValueOrCreateIIFE(prop.value)
),
prop
);
// Collects `childContextTypes`, `contextTypes`, `displayName`, and `propTypes`;
// simplifies `getDefaultProps` or converts it to an IIFE;
// and collects everything else in the `statics` property object.
const collectStatics = specPath => {
const result = [];
for (let i = 0; i < specPath.properties.length; i++) {
const property = specPath.properties[i];
if (createFindPropFn('statics')(property) && property.value && property.value.properties) {
result.push(...property.value.properties);
} else if (createFindPropFn(DEFAULT_PROPS_FIELD)(property)) {
result.push(createDefaultProps(property));
} else if (property.key && STATIC_KEYS.hasOwnProperty(property.key.name)) {
result.push(property);
}
}
return result;
};
const collectNonStaticProperties = specPath => specPath.properties
.filter(prop =>
!(filterDefaultPropsField(prop) || filterGetInitialStateField(prop))
)
.filter(prop => (!STATIC_KEYS.hasOwnProperty(prop.key.name)) && prop.key.name !== STATIC_KEY)
.filter(prop =>
isFunctionExpression(prop) ||
isPrimPropertyWithTypeAnnotation(prop) ||
isPrimProperty(prop)
);
const findRequirePathAndBinding = (moduleName) => {
let result = null;
const requireCall = root.find(j.VariableDeclarator, {
id: {type: 'Identifier'},
init: {
callee: {name: 'require'},
arguments: [{value: moduleName}],
},
});
const importStatement = root.find(j.ImportDeclaration, {
source: {
value: moduleName,
},
});
if (importStatement.size()) {
importStatement.forEach(path => {
result = {
path,
binding: path.value.specifiers[0].local.name,
type: 'import',
};
});
} else if (requireCall.size()) {
requireCall.forEach(path => {
result = {
path,
binding: path.value.id.name,
type: 'require',
};
});
}
return result;
};
const pureRenderMixinPathAndBinding = findRequirePathAndBinding(PURE_MIXIN_MODULE_NAME);
// ---------------------------------------------------------------------------
// Boom!
const createMethodDefinition = fn =>
withComments(j.methodDefinition(
'method',
fn.key,
fn.value
), fn);
const updatePropsAndContextAccess = getInitialState => {
const collection = j(getInitialState);
collection.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
type: 'Identifier',
name: 'props',
},
}).forEach(path => j(path).replaceWith(j.identifier('props')));
collection.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
type: 'Identifier',
name: 'context',
},
}).forEach(path => j(path).replaceWith(j.identifier('context')));
};
const inlineGetInitialState = getInitialState => {
const functionExpressionCollection = j(getInitialState.value);
// at this point if there exists bindings like `const props = ...`, we
// already know the RHS must be `this.props` (see `isGetInitialStateConstructorSafe`)
// so it's safe to just remove them
functionExpressionCollection.find(j.VariableDeclarator, {id: {name: 'props'}})
.forEach(path => j(path).remove());
functionExpressionCollection.find(j.VariableDeclarator, {id: {name: 'context'}})
.forEach(path => j(path).remove());
return functionExpressionCollection
.find(j.ReturnStatement)
.filter(path => {
// filter out inner function declarations here (helper functions, promises, etc.).
const mainBodyCollection = j(getInitialState.value.body);
return (
mainBodyCollection
.find(j.ArrowFunctionExpression)
.find(j.ReturnStatement, path.value)
.size() === 0 &&
mainBodyCollection
.find(j.FunctionDeclaration)
.find(j.ReturnStatement, path.value)
.size() === 0 &&
mainBodyCollection
.find(j.FunctionExpression)
.find(j.ReturnStatement, path.value)
.size() === 0
);
})
.forEach(path => {
let shouldInsertReturnAfterAssignment = false;
// if the return statement is not a direct child of getInitialState's body
if (getInitialState.value.body.body.indexOf(path.value) === -1) {
shouldInsertReturnAfterAssignment = true;
}
j(path).replaceWith(j.expressionStatement(
j.assignmentExpression(
'=',
j.memberExpression(
j.thisExpression(),
j.identifier('state'),
false
),
path.value.argument
)
));
if (shouldInsertReturnAfterAssignment) {
j(path).insertAfter(j.returnStatement(null));
}
}).getAST()[0].value.body.body;
};
const convertInitialStateToClassProperty = getInitialState =>
withComments(j.classProperty(
j.identifier('state'),
pickReturnValueOrCreateIIFE(getInitialState.value),
getInitialState.value.returnType,
false
), getInitialState);
const createConstructorArgs = (hasContextAccess) => {
if (hasContextAccess) {
return [j.identifier('props'), j.identifier('context')];
}
return [j.identifier('props')];
};
const createConstructor = (getInitialState) => {
const initialStateAST = j(getInitialState);
let hasContextAccess = false;
if (
initialStateAST.find(j.MemberExpression, { // has `this.context` access
object: {type: 'ThisExpression'},
property: {type: 'Identifier', name: 'context'},
}).size() ||
initialStateAST.find(j.CallExpression, { // a direct method call `this.x()`
callee: {
type: 'MemberExpression',
object: {type: 'ThisExpression'},
},
}).size() ||
initialStateAST.find(j.MemberExpression, { // `this` is referenced alone
object: {type: 'ThisExpression'},
}).size() !== initialStateAST.find(j.ThisExpression).size()
) {
hasContextAccess = true;
}
updatePropsAndContextAccess(getInitialState);
const constructorArgs = createConstructorArgs(hasContextAccess);
return [
createMethodDefinition({
key: j.identifier('constructor'),
value: j.functionExpression(
null,
constructorArgs,
j.blockStatement(
[].concat(
[
j.expressionStatement(
j.callExpression(
j.identifier('super'),
constructorArgs
)
),
],
inlineGetInitialState(getInitialState)
)
)
),
}),
];
};
const createArrowFunctionExpression = fn => {
const arrowFunc = j.arrowFunctionExpression(
fn.params,
fn.body,
false
);
arrowFunc.returnType = fn.returnType;
arrowFunc.defaults = fn.defaults;
arrowFunc.rest = fn.rest;
arrowFunc.async = fn.async;
arrowFunc.generator = fn.generator;
return arrowFunc;
};
const createArrowProperty = prop =>
withComments(j.classProperty(
j.identifier(prop.key.name),
createArrowFunctionExpression(prop.value),
null,
false
), prop);
const createClassProperty = prop =>
withComments(j.classProperty(
j.identifier(prop.key.name),
prop.value,
null,
false
), prop);
const createClassPropertyWithType = prop =>
withComments(j.classProperty(
j.identifier(prop.key.name),
prop.value.expression,
prop.value.typeAnnotation,
false
), prop);
// ---------------------------------------------------------------------------
// Flow!
const flowAnyType = j.anyTypeAnnotation();
const flowFixMeType = j.genericTypeAnnotation(
j.identifier('$FlowFixMe'),
null
);
const literalToFlowType = node => {
if (node.type === 'Identifier' && node.name === 'undefined') {
return j.voidTypeAnnotation();
}
switch (typeof node.value) {
case 'string':
return j.stringLiteralTypeAnnotation(node.value, node.raw);
case 'number':
return j.numberLiteralTypeAnnotation(node.value, node.raw);
case 'boolean':
return j.booleanLiteralTypeAnnotation(node.value, node.raw);
case 'object': // we already know it's a NullLiteral here
return j.nullLiteralTypeAnnotation();
default: // this should never happen
return flowFixMeType;
}
};
const propTypeToFlowMapping = {
// prim types
any: flowAnyType,
array: j.genericTypeAnnotation(
j.identifier('Array'),
j.typeParameterInstantiation([flowFixMeType])
),
bool: j.booleanTypeAnnotation(),
element: flowFixMeType, // flow does the same for `element` type in `propTypes`
func: j.genericTypeAnnotation(
j.identifier('Function'),
null
),
node: flowFixMeType, // flow does the same for `node` type in `propTypes`
number: j.numberTypeAnnotation(),
object: j.genericTypeAnnotation(
j.identifier('Object'),
null
),
string: j.stringTypeAnnotation(),
// type classes
arrayOf: (type) => j.genericTypeAnnotation(
j.identifier('Array'),
j.typeParameterInstantiation([type])
),
instanceOf: (type) => j.genericTypeAnnotation(
type,
null
),
objectOf: (type) => j.objectTypeAnnotation([], [
j.objectTypeIndexer(
j.identifier('key'),
j.stringTypeAnnotation(),
type
)
]),
oneOf: (typeList) => j.unionTypeAnnotation(typeList),
oneOfType: (typeList) => j.unionTypeAnnotation(typeList),
shape: (propList) => j.objectTypeAnnotation(propList),
};
const propTypeToFlowAnnotation = val => {
let cursor = val;
let isOptional = true;
let typeResult = flowFixMeType;
if ( // check `.isRequired` first
cursor.type === 'MemberExpression' &&
cursor.property.type === 'Identifier' &&
cursor.property.name === 'isRequired'
) {
isOptional = false;
cursor = cursor.object;
}
switch (cursor.type) {
case 'CallExpression': { // type class
const calleeName = cursor.callee.type === 'MemberExpression' ?
cursor.callee.property.name :
cursor.callee.name;
const constructor = propTypeToFlowMapping[calleeName];
if (!constructor) { // unknown type class
// it's not necessary since `typeResult` defaults to `flowFixMeType`,
// but it's more explicit this way
typeResult = flowFixMeType;
break;
}
switch (cursor.callee.property.name) {
case 'arrayOf': {
const arg = cursor.arguments[0];
typeResult = constructor(
propTypeToFlowAnnotation(arg)[0]
);
break;
}
case 'instanceOf': {
const arg = cursor.arguments[0];
if (arg.type !== 'Identifier') {
typeResult = flowFixMeType;
break;
}
typeResult = constructor(arg);
break;
}
case 'objectOf': {
const arg = cursor.arguments[0];
typeResult = constructor(
propTypeToFlowAnnotation(arg)[0]
);
break;
}
case 'oneOf': {
const argList = cursor.arguments[0].elements;
if (
!argList ||
!argList.every(node =>
(node.type === 'Literal') ||
(node.type === 'Identifier' && node.name === 'undefined')
)
) {
typeResult = flowFixMeType;
} else {
typeResult = constructor(
argList.map(literalToFlowType)
);
}
break;
}
case 'oneOfType': {
const argList = cursor.arguments[0].elements;
if (!argList) {
typeResult = flowFixMeType;
} else {
typeResult = constructor(
argList.map(arg => propTypeToFlowAnnotation(arg)[0])
);
}
break;
}
case 'shape': {
const rawPropList = cursor.arguments[0].properties;
if (!rawPropList) {
typeResult = flowFixMeType;
break;
}
const flowPropList = [];
rawPropList.forEach(typeProp => {
const keyIsLiteral = typeProp.key.type === 'Literal';
const name = keyIsLiteral ? typeProp.key.value : typeProp.key.name;
const [valueType, isOptional] = propTypeToFlowAnnotation(typeProp.value);
flowPropList.push(j.objectTypeProperty(
keyIsLiteral ? j.literal(name) : j.identifier(name),
valueType,
isOptional
));
});
typeResult = constructor(flowPropList);
break;
}
default: {
break;
}
}
break;
}
case 'MemberExpression': { // prim type
if (cursor.property.type !== 'Identifier') { // unrecognizable
typeResult = flowFixMeType;
break;
}
const maybeType = propTypeToFlowMapping[cursor.property.name];
if (maybeType) {
typeResult = propTypeToFlowMapping[cursor.property.name];
} else { // type not found
typeResult = flowFixMeType;
}
break;
}
default: { // unrecognizable
break;
}
}
return [typeResult, isOptional];
};
const createFlowAnnotationsFromPropTypesProperties = (prop) => {
const typePropertyList = [];
if (!prop || prop.value.type !== 'ObjectExpression') {
return typePropertyList;
}
prop.value.properties.forEach(typeProp => {
if (!typeProp.key) { // stuff like SpreadProperty
return;
}
const keyIsLiteral = typeProp.key.type === 'Literal';
const name = keyIsLiteral ? typeProp.key.value : typeProp.key.name;
const [valueType, isOptional] = propTypeToFlowAnnotation(typeProp.value);
typePropertyList.push(j.objectTypeProperty(
keyIsLiteral ? j.literal(name) : j.identifier(name),
valueType,
isOptional
));
});
return j.classProperty(
j.identifier('props'),
null,
j.typeAnnotation(j.objectTypeAnnotation(typePropertyList)),
false
);
};
// to ensure that our property initializers' evaluation order is safe
const repositionStateProperty = (initialStateProperty, propertiesAndMethods) => {
const initialStateCollection = j(initialStateProperty);
const thisCount = initialStateCollection.find(j.ThisExpression).size();
const safeThisMemberCount = initialStateCollection.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
type: 'Identifier',
name: 'props',
},
}).size() + initialStateCollection.find(j.MemberExpression, {
object: {
type: 'ThisExpression',
},
property: {
type: 'Identifier',
name: 'context',
},
}).size();
if (thisCount === safeThisMemberCount) {
return initialStateProperty.concat(propertiesAndMethods);
}
const result = [].concat(propertiesAndMethods);
let lastPropPosition = result.length - 1;
while (lastPropPosition >= 0 && result[lastPropPosition].kind === 'method') {
lastPropPosition--;
}
result.splice(lastPropPosition + 1, 0, initialStateProperty[0]);
return result;
};
// if there's no `getInitialState` or the `getInitialState` function is simple
// (i.e., it's just a return statement) then we don't need a constructor.
// we can simply lift `state = {...}` as a property initializer.
// otherwise, create a constructor and inline `this.state = ...`.
//
// when we need to create a constructor, we only put `context` as the
// second parameter when the following things happen in `getInitialState()`:
// 1. there's a `this.context` access, or
// 2. there's a direct method call `this.x()`, or
// 3. `this` is referenced alone
const createESClass = (
name,
baseClassName,
staticProperties,
getInitialState,
rawProperties,
comments
) => {
const initialStateProperty = [];
let maybeConstructor = [];
let maybeFlowStateAnnotation = []; // we only need this when we do `this.state = ...`
if (isInitialStateLiftable(getInitialState)) {
if (getInitialState) {
initialStateProperty.push(convertInitialStateToClassProperty(getInitialState));
}
} else {
maybeConstructor = createConstructor(getInitialState);
if (shouldTransformFlow) {
let stateType = j.typeAnnotation(
j.existsTypeAnnotation()
);
if (getInitialState.value.returnType) {
stateType = getInitialState.value.returnType;
}
maybeFlowStateAnnotation.push(j.classProperty(
j.identifier('state'),
null,
stateType,
false
));
}
}
const propertiesAndMethods = rawProperties.map(prop => {
if (isPrimPropertyWithTypeAnnotation(prop)) {
return createClassPropertyWithType(prop);
} else if (isPrimProperty(prop)) {
return createClassProperty(prop);
} else if (AUTOBIND_IGNORE_KEYS.hasOwnProperty(prop.key.name)) {
return createMethodDefinition(prop);
}
return createArrowProperty(prop);
});
const flowPropsAnnotation = shouldTransformFlow ?
createFlowAnnotationsFromPropTypesProperties(
staticProperties.find((path) => path.key.name === 'propTypes')
) :
[];
let finalStaticProperties = staticProperties;
if (shouldTransformFlow && options['remove-runtime-proptypes']) {
finalStaticProperties = staticProperties.filter((prop) => prop.key.name !== 'propTypes');
}
return withComments(j.classDeclaration(
name ? j.identifier(name) : null,
j.classBody(
[].concat(
flowPropsAnnotation,
maybeFlowStateAnnotation,
finalStaticProperties,
maybeConstructor,
repositionStateProperty(initialStateProperty, propertiesAndMethods)
)
),
j.memberExpression(
j.identifier('React'),
j.identifier(baseClassName),
false
)
), {comments});
};
const createStaticClassProperty = staticProperty => {
if (staticProperty.value.type === 'FunctionExpression') {
return withComments(j.methodDefinition(
'method',
j.identifier(staticProperty.key.name),
staticProperty.value,
true
), staticProperty);
}
if (staticProperty.value.type === 'TypeCastExpression') {
return withComments(j.classProperty(
j.identifier(staticProperty.key.name),
staticProperty.value.expression,
staticProperty.value.typeAnnotation,
true
), staticProperty);
}
return withComments(j.classProperty(
j.identifier(staticProperty.key.name),
staticProperty.value,
null,
true
), staticProperty);
};
const createStaticClassProperties = statics =>
statics.map(createStaticClassProperty);
const getComments = classPath => {
if (classPath.value.comments) {
return classPath.value.comments;
}
const declaration = j(classPath).closest(j.VariableDeclaration);
if (declaration.size()) {
return declaration.get().value.comments;
}
return null;
};
const findUnusedVariables = (path, varName) => j(path)
.closestScope()
.find(j.Identifier, {name: varName})
// Ignore require vars
.filter(identifierPath => identifierPath.value !== path.value.id)
// Ignore import bindings
.filter(identifierPath => !(
path.value.type === 'ImportDeclaration' &&
path.value.specifiers.some(specifier => specifier.local === identifierPath.value)
))
// Ignore properties in MemberExpressions
.filter(identifierPath => {
const parent = identifierPath.parent.value;
return !(
j.MemberExpression.check(parent) &&
parent.property === identifierPath.value
);
});
const updateToClass = (classPath) => {
const specPath = ReactUtils.directlyGetCreateClassSpec(classPath);
const name = ReactUtils.directlyGetComponentName(classPath);
const statics = collectStatics(specPath);
const properties = collectNonStaticProperties(specPath);
const comments = getComments(classPath);
const getInitialState = findGetInitialState(specPath);
var path = classPath;
if (
classPath.parentPath &&
classPath.parentPath.value &&
classPath.parentPath.value.type === 'VariableDeclarator'
) {
// the reason that we need to do this awkward dance here is that
// for things like `var Foo = React.createClass({...})`, we need to
// replace the _entire_ VariableDeclaration with
// `class Foo extends React.Component {...}`.
// it looks scary but since we already know it's a VariableDeclarator
// it's actually safe.
// (VariableDeclaration > declarations > VariableDeclarator > CallExpression)
path = classPath.parentPath.parentPath.parentPath;
}
const staticProperties = createStaticClassProperties(statics);
const baseClassName =
pureRenderMixinPathAndBinding &&
ReactUtils.directlyHasSpecificMixins(classPath, [pureRenderMixinPathAndBinding.binding]) ?
'PureComponent' :
'Component';
j(path).replaceWith(
createESClass(
name,
baseClassName,
staticProperties,
getInitialState,
properties,
comments
)
);
};
const addDisplayName = (displayName, specPath) => {
const props = specPath.properties;
let safe = true;
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (prop.key.name === 'displayName') {
safe = false;
break;
}
}
if (safe) {
props.unshift(j.objectProperty(j.identifier('displayName'), j.stringLiteral(displayName)));
}
};
const fallbackToCreateClassModule = (classPath) => {
const comments = getComments(classPath);
const specPath = ReactUtils.directlyGetCreateClassSpec(classPath);
if (!NO_DISPLAY_NAME) {
if (specPath) {
// Add a displayName property to the spec object
let path = classPath;
let displayName;
while (path && displayName === undefined) {
switch (path.node.type) {
case 'ExportDefaultDeclaration':
displayName = basename(file.path, extname(file.path));
if (displayName === 'index') {
// ./{module name}/index.js
displayName = basename(dirname(file.path));
}
break;
case 'VariableDeclarator':
displayName = path.node.id.name;
break;
case 'AssignmentExpression':
displayName = path.node.left.name;
break;
case 'Property':
displayName = path.node.key.name;
break;
case 'Statement':
displayName = null;
break;
}
path = path.parent;
}
if (displayName) {
addDisplayName(displayName, specPath);
}
}
}
withComments(
j(classPath).replaceWith(
specPath
? j.callExpression(j.identifier(CREATE_CLASS_VARIABLE_NAME), [specPath])
: j.callExpression(j.identifier(CREATE_CLASS_VARIABLE_NAME), classPath.value.arguments)
),
{comments},
);
};
if (
options['explicit-require'] === false || ReactUtils.hasReact(root)
) {
// no mixins found on the classPath -> true
// pure mixin identifier not found -> (has mixins) -> false
// found pure mixin identifier ->
// class mixins is an array and only contains the identifier -> true
// otherwise -> false
const mixinsFilter = (classPath) => {
if (!ReactUtils.directlyHasMixinsField(classPath)) {
return true;
} else if (options['pure-component'] && pureRenderMixinPathAndBinding) {
const {binding} = pureRenderMixinPathAndBinding;
if (areMixinsConvertible([binding], classPath)) {
return true;
}
}
console.warn(
file.path + ': `' + ReactUtils.directlyGetComponentName(classPath) + '` ' +
'was skipped because of inconvertible mixins.'
);
return false;
};
const reinsertTopComments = () => {
root.get().node.comments = topComments;
};
let didTransform = false;
const path = ReactUtils.findAllReactCreateClassCalls(root);
if (NO_CONVERSION) {
throw new Error("Unsupported NO_CONVERSION");
} else {
// the only time that we can't simply replace the createClass call path
// with a new class is when the parent of that is a variable declaration.
// let's delay it and figure it out later (by looking at `path.parentPath`)
// in `updateToClass`.
path.forEach(childPath => {
if (
mixinsFilter(childPath) &&
hasNoCallsToDeprecatedAPIs(childPath) &&
hasNoRefsToAPIsThatWillBeRemoved(childPath) &&
doesNotUseArguments(childPath) &&
isInitialStateConvertible(childPath) &&
canConvertToClass(childPath)
) {
didTransform = true;
updateToClass(childPath);
}
});
}
if (didTransform) {
// prune removed requires
if (pureRenderMixinPathAndBinding) {
const {binding, path, type} = pureRenderMixinPathAndBinding;
let shouldReinsertComment = false;
if (findUnusedVariables(path, binding).size() === 0) {
var removePath = null;
if (type === 'require') {
const bodyNode = path.parentPath.parentPath.parentPath.value;
const variableDeclarationNode = path.parentPath.parentPath.value;
removePath = path.parentPath.parentPath;
shouldReinsertComment = bodyNode.indexOf(variableDeclarationNode) === 0;
} else {
const importDeclarationNode = path.value;
const bodyNode = path.parentPath.value;
removePath = path;
shouldReinsertComment = bodyNode.indexOf(importDeclarationNode) === 0;
}
j(removePath).remove();
if (shouldReinsertComment) {
reinsertTopComments();
}
}
}
}
}
return root.toSource(printOptions);
};
module.exports.parser = 'flow';
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment