react-docgen
Version:
A library to extract information from React components for documentation generation.
172 lines (171 loc) • 7.38 kB
JavaScript
import { Scope, visitors } from '@babel/traverse';
import getMemberExpressionRoot from './getMemberExpressionRoot.js';
import getPropertyValuePath from './getPropertyValuePath.js';
import { Array as toArray } from './expressionTo.js';
import { shallowIgnoreVisitors } from './traverse.js';
import getMemberValuePath, { isSupportedDefinitionType, } from './getMemberValuePath.js';
import initialize from './ts-types/index.js';
import getNameOrValue from './getNameOrValue.js';
function findScopePath(bindingIdentifiers) {
if (bindingIdentifiers && bindingIdentifiers[0]) {
const resolvedParentPath = bindingIdentifiers[0].parentPath;
if (resolvedParentPath.isImportDefaultSpecifier() ||
resolvedParentPath.isImportSpecifier()) {
let exportName;
if (resolvedParentPath.isImportDefaultSpecifier()) {
exportName = 'default';
}
else {
const imported = resolvedParentPath.get('imported');
if (imported.isStringLiteral()) {
exportName = imported.node.value;
}
else if (imported.isIdentifier()) {
exportName = imported.node.name;
}
}
if (!exportName) {
throw new Error('Could not detect export name');
}
const importedPath = resolvedParentPath.hub.import(resolvedParentPath.parentPath, exportName);
if (importedPath) {
return resolveToValue(importedPath);
}
}
return resolveToValue(resolvedParentPath);
}
return null;
}
const explodedVisitors = visitors.explode({
...shallowIgnoreVisitors,
AssignmentExpression: {
enter: function (assignmentPath, state) {
const node = state.idPath.node;
const left = assignmentPath.get('left');
// Skip anything that is not an assignment to a variable with the
// passed name.
// Ensure the LHS isn't the reference we're trying to resolve.
if (!left.isIdentifier() ||
left.node === node ||
left.node.name !== node.name ||
assignmentPath.node.operator !== '=') {
return assignmentPath.skip();
}
// Ensure the RHS doesn't contain the reference we're trying to resolve.
const candidatePath = assignmentPath.get('right');
if (candidatePath.node === node ||
state.idPath.findParent((parent) => parent.node === candidatePath.node)) {
return assignmentPath.skip();
}
state.resultPath = candidatePath;
return assignmentPath.skip();
},
},
});
/**
* Tries to find the last value assigned to `name` in the scope created by
* `scope`. We are not descending into any statements (blocks).
*/
function findLastAssignedValue(path, idPath) {
const state = { idPath };
path.traverse(explodedVisitors, state);
return state.resultPath ? resolveToValue(state.resultPath) : null;
}
/**
* If the path is an identifier, it is resolved in the scope chain.
* If it is an assignment expression, it resolves to the right hand side.
* If it is a member expression it is resolved to it's initialization value.
*
* Else the path itself is returned.
*/
export default function resolveToValue(path) {
if (path.isIdentifier()) {
if ((path.parentPath.isClass() || path.parentPath.isFunction()) &&
path.parentPath.get('id') === path) {
return path.parentPath;
}
const binding = path.scope.getBinding(path.node.name);
let resolvedPath = null;
if (binding) {
// The variable may be assigned a different value after initialization.
// We are first trying to find all assignments to the variable in the
// block where it is defined (i.e. we are not traversing into statements)
resolvedPath = findLastAssignedValue(binding.scope.path, path);
if (!resolvedPath) {
const bindingMap = binding.path.getOuterBindingIdentifierPaths(true);
resolvedPath = findScopePath(bindingMap[path.node.name]);
}
}
else {
// Initialize our monkey-patching of @babel/traverse 🙈
initialize(Scope);
const typeBinding = path.scope.getTypeBinding(path.node.name);
if (typeBinding) {
resolvedPath = findScopePath([typeBinding.identifierPath]);
}
}
return resolvedPath || path;
}
else if (path.isVariableDeclarator()) {
const init = path.get('init');
if (init.hasNode()) {
return resolveToValue(init);
}
}
else if (path.isMemberExpression()) {
const root = getMemberExpressionRoot(path);
const resolved = resolveToValue(root);
if (resolved.isObjectExpression()) {
let propertyPath = resolved;
for (const propertyName of toArray(path).slice(1)) {
if (propertyPath && propertyPath.isObjectExpression()) {
propertyPath = getPropertyValuePath(propertyPath, propertyName);
}
if (!propertyPath) {
return path;
}
propertyPath = resolveToValue(propertyPath);
}
return propertyPath;
}
else if (isSupportedDefinitionType(resolved)) {
const property = path.get('property');
if (property.isIdentifier() || property.isStringLiteral()) {
const memberPath = getMemberValuePath(resolved, property.isIdentifier() ? property.node.name : property.node.value);
if (memberPath) {
return resolveToValue(memberPath);
}
}
}
else if (resolved.isImportSpecifier() ||
resolved.isImportDefaultSpecifier() ||
resolved.isImportNamespaceSpecifier()) {
const declaration = resolved.parentPath;
// Handle references to namespace imports, e.g. import * as foo from 'bar'.
// Try to find a specifier that matches the root of the member expression, and
// find the export that matches the property name.
for (const specifier of declaration.get('specifiers')) {
const property = path.get('property');
let propertyName;
if (property.isIdentifier() || property.isStringLiteral()) {
propertyName = getNameOrValue(property);
}
if (specifier.isImportNamespaceSpecifier() &&
root.isIdentifier() &&
propertyName &&
specifier.node.local.name === root.node.name) {
const resolvedPath = path.hub.import(declaration, propertyName);
if (resolvedPath) {
return resolveToValue(resolvedPath);
}
}
}
}
}
else if (path.isTypeCastExpression() ||
path.isTSAsExpression() ||
path.isTSTypeAssertion()) {
return resolveToValue(path.get('expression'));
}
return path;
}