src/utils/resolveObjectKeysToArray.ts (103 lines of code) (raw):

import { ASTNode, namedTypes as t } from 'ast-types'; import type { NodePath } from 'ast-types/lib/node-path'; import resolveToValue from './resolveToValue'; import type { Importer } from '../parse'; function isObjectKeysCall(node: ASTNode): boolean { return ( t.CallExpression.check(node) && node.arguments.length === 1 && t.MemberExpression.check(node.callee) && t.Identifier.check(node.callee.object) && node.callee.object.name === 'Object' && t.Identifier.check(node.callee.property) && node.callee.property.name === 'keys' ); } function isWhitelistedObjectProperty(prop) { return ( (t.Property.check(prop) && ((t.Identifier.check(prop.key) && !prop.computed) || t.Literal.check(prop.key))) || t.SpreadElement.check(prop) ); } function isWhiteListedObjectTypeProperty(prop) { return ( t.ObjectTypeProperty.check(prop) || t.ObjectTypeSpreadProperty.check(prop) || t.TSPropertySignature.check(prop) ); } // Resolves an ObjectExpression or an ObjectTypeAnnotation export function resolveObjectToNameArray( object: NodePath, importer: Importer, raw = false, ): string[] | null { if ( (t.ObjectExpression.check(object.value) && object.value.properties.every(isWhitelistedObjectProperty)) || (t.ObjectTypeAnnotation.check(object.value) && object.value.properties.every(isWhiteListedObjectTypeProperty)) || (t.TSTypeLiteral.check(object.value) && object.value.members.every(isWhiteListedObjectTypeProperty)) ) { let values: string[] = []; let error = false; const properties = t.TSTypeLiteral.check(object.value) ? object.get('members') : object.get('properties'); properties.each((propPath: NodePath) => { if (error) return; const prop = propPath.value; if ( t.Property.check(prop) || t.ObjectTypeProperty.check(prop) || t.TSPropertySignature.check(prop) ) { // Key is either Identifier or Literal // @ts-ignore const name = prop.key.name || (raw ? prop.key.raw : prop.key.value); values.push(name); } else if ( t.SpreadElement.check(prop) || t.ObjectTypeSpreadProperty.check(prop) ) { let spreadObject = resolveToValue(propPath.get('argument'), importer); if (t.GenericTypeAnnotation.check(spreadObject.value)) { const typeAlias = resolveToValue(spreadObject.get('id'), importer); if (t.ObjectTypeAnnotation.check(typeAlias.get('right').value)) { spreadObject = resolveToValue(typeAlias.get('right'), importer); } } const spreadValues = resolveObjectToNameArray(spreadObject, importer); if (!spreadValues) { error = true; return; } values = [...values, ...spreadValues]; } }); if (!error) { return values; } } return null; } /** * Returns an ArrayExpression which contains all the keys resolved from an object * * Ignores setters in objects * * Returns null in case of * unresolvable spreads * computed identifier keys */ export default function resolveObjectKeysToArray( path: NodePath, importer: Importer, ): string[] | null { const node = path.node; if (isObjectKeysCall(node)) { const objectExpression = resolveToValue( path.get('arguments').get(0), importer, ); const values = resolveObjectToNameArray(objectExpression, importer); if (values) { const nodes = values .filter((value, index, array) => array.indexOf(value) === index) .map(value => `"${value}"`); return nodes; } } return null; }