in transforms/pure-component.js [11:339]
module.exports = function(file, api, options) {
const j = api.jscodeshift;
const ReactUtils = require('./utils/ReactUtils')(j);
const useArrows = options.useArrows || false;
const destructuringEnabled = options.destructuring || false;
const silenceWarnings = options.silenceWarnings || false;
const printOptions = options.printOptions || {
quote: 'single',
trailingComma: true
};
const getClassName = path => path.node.id.name;
const isRenderMethod = node =>
node.type == 'MethodDefinition' &&
node.key.type == 'Identifier' &&
node.key.name == 'render';
const isPropsProperty = node =>
node.type === 'ClassProperty' &&
node.key.type === 'Identifier' &&
node.key.name === 'props';
const isStaticProperty = node => node.type === 'ClassProperty' && node.static;
const onlyHasRenderMethod = path =>
j(path)
.find(j.MethodDefinition)
.filter(p => !isRenderMethod(p.value))
.size() === 0;
const onlyHasSafeClassProperties = path =>
j(path)
.find(j.ClassProperty)
.filter(p => !(isPropsProperty(p.value) || isStaticProperty(p.value)))
.size() === 0;
const hasRefs = path =>
j(path)
.find(j.JSXAttribute, {
name: {
type: 'JSXIdentifier',
name: 'ref'
}
})
.size() > 0;
const THIS_PROPS = {
object: {
type: 'ThisExpression'
},
property: {
name: 'props'
}
};
const replaceThisProps = path =>
j(path)
.find(j.MemberExpression, THIS_PROPS)
.replaceWith(j.identifier('props'));
const buildIdentifierWithTypeAnnotation = (name, typeAnnotation) => {
const identifier = j.identifier(name);
if (typeAnnotation) {
identifier.typeAnnotation = j.typeAnnotation(typeAnnotation);
}
return identifier;
};
const isDuplicateDeclaration = (path, pre) => {
if (path && path.value && path.value.id && path.value.init) {
const initName = pre
? path.value.init.property && path.value.init.property.name
: path.value.init.name;
return path.value.id.name === initName;
}
return false;
};
const needsThisDotProps = path =>
path
.find(j.Identifier, {
name: 'props'
})
.filter(p => p.parentPath.parentPath.value.type !== 'MemberExpression')
.size() > 0;
const getPropNames = path => {
const propNames = new Set();
path
.find(j.MemberExpression, {
object: {
property: {
name: 'props'
}
}
})
.forEach(p => {
propNames.add(p.value.property.name);
});
return propNames;
};
const getDuplicateNames = path => {
const duplicates = new Set();
path
.find(j.VariableDeclarator)
.filter(p => isDuplicateDeclaration(p, true))
.forEach(p => {
duplicates.add(p.value.id.name);
});
return duplicates;
};
const getAssignmentNames = path => {
const assignmentNames = new Set();
path
.find(j.Identifier)
.filter(p => {
if (p.value.type === 'JSXIdentifier') {
return false;
}
if (
!(p.parentPath.value.object && p.parentPath.value.object.property)
) {
return true;
}
return p.parentPath.value.object.property.name !== 'props';
})
.forEach(p => {
assignmentNames.add(p.value.name);
});
return assignmentNames;
};
const hasAssignmentsThatShadowProps = path => {
const propNames = getPropNames(path);
const assignmentNames = getAssignmentNames(path);
const duplicates = getDuplicateNames(path);
return Array.from(propNames).some(
prop => !duplicates.has(prop) && assignmentNames.has(prop)
);
};
const canDestructure = path =>
!needsThisDotProps(path) && !hasAssignmentsThatShadowProps(path);
const createShorthandProperty = (j, typeAnnotation) => prop => {
const property = j.property('init', j.identifier(prop), j.identifier(prop));
property.shorthand = true;
if (typeAnnotation) {
typeAnnotation.properties.forEach(t => {
if (t.key.name === prop) {
property.key.typeAnnotation = j.typeAnnotation(t.value);
}
});
}
return property;
};
const destructureProps = (body, typeAnnotation) => {
const toDestructure = body.find(j.MemberExpression, {
object: {
name: 'props'
}
});
if (toDestructure) {
const propNames = new Set();
toDestructure.replaceWith(path => {
const propName = path.value.property.name;
propNames.add(propName);
return j.identifier(propName);
});
if (propNames.size > 0) {
const assignments = body.find(j.VariableDeclarator);
const duplicateAssignments = assignments.filter(a =>
isDuplicateDeclaration(a, false)
);
duplicateAssignments.remove();
return j.objectPattern(
Array.from(propNames).map(createShorthandProperty(j, typeAnnotation))
);
}
}
return false;
};
const findPropsTypeAnnotation = body => {
const property = body.find(isPropsProperty);
return property && property.typeAnnotation.typeAnnotation;
};
const isDefaultExport = path =>
path.parentPath && path.parentPath.value.type === 'ExportDefaultDeclaration';
const safelyDefaultExportDeclaration = (path) => {
const localName = path.value.declarations[0].id.name;
j(path.parent)
.replaceWith(_ => path.value)
.insertAfter(
j.exportDeclaration(true, { type: 'Identifier', name: localName })
);
};
const build = useArrows => (name, body, typeAnnotation, destructure, hasThisDotProps) => {
const identifier = j.identifier(name);
const propsIdentifier = buildIdentifierWithTypeAnnotation(
'props',
typeAnnotation
);
const propsArg = hasThisDotProps ? [
(destructure && destructureProps(j(body), typeAnnotation)) ||
propsIdentifier
] : [];
if (useArrows) {
return j.variableDeclaration('const', [
j.variableDeclarator(
identifier,
j.arrowFunctionExpression(propsArg, body)
)
]);
}
return j.functionDeclaration(identifier, propsArg, body);
};
const buildPureComponentFunction = build();
const buildPureComponentArrowFunction = build(true);
const buildStatics = (name, properties) =>
properties.map(prop =>
j.expressionStatement(
j.assignmentExpression(
'=',
j.memberExpression(j.identifier(name), prop.key),
prop.value
)
)
);
const reportSkipped = path => {
const name = getClassName(path);
const fileName = file.path;
if (!path.value.loc) {
console.warn(`Class "${name}" skipped in ${fileName}`);
return;
}
const { line, column } = path.value.loc.start;
console.warn(`Class "${name}" skipped in ${fileName} on ${line}:${column}`);
};
const f = j(file.source);
const pureClasses = ReactUtils.findReactES6ClassDeclaration(f).filter(
path => {
const isPure =
onlyHasRenderMethod(path) &&
onlyHasSafeClassProperties(path) &&
!hasRefs(path);
if (!isPure && !silenceWarnings) {
reportSkipped(path);
}
return isPure;
}
);
if (pureClasses.size() === 0) {
return null;
}
// Save the names of the deleted pure classes super class
// We need this to prune unused variables at the end.
const parentClassNames = pureClasses.nodes().map(node => node.superClass.name);
pureClasses.replaceWith(p => {
const name = p.node.id.name;
const renderMethod = p.value.body.body.filter(isRenderMethod)[0];
const renderBody = renderMethod.value.body;
const propsTypeAnnotation = findPropsTypeAnnotation(p.value.body.body);
const statics = p.value.body.body.filter(isStaticProperty);
const destructure = destructuringEnabled && canDestructure(j(renderMethod));
if (destructuringEnabled && !destructure) {
console.warn(`Unable to destructure ${name} props.`);
}
const hasThisDotProps = j(renderBody).find(j.MemberExpression, THIS_PROPS).length > 0;
replaceThisProps(renderBody);
if (useArrows) {
return [
buildPureComponentArrowFunction(
name,
renderBody,
propsTypeAnnotation,
destructure,
hasThisDotProps
),
...buildStatics(name, statics)
];
} else {
return [
buildPureComponentFunction(
name,
renderBody,
propsTypeAnnotation,
destructure,
hasThisDotProps
),
...buildStatics(name, statics)
];
}
}).forEach(p => {
// Check for combining default keyword with const declaration
if (useArrows && isDefaultExport(p)) {
safelyDefaultExportDeclaration(p);
}
}).forEach((p, i) => {
const parentClassName = parentClassNames[i];
ReactUtils.removeUnusedSuperClassImport(j(p), f, parentClassName);
});
return f.toSource(printOptions);
};