function getPropTypesForNode()

in packages/eui/scripts/babel/proptypes-from-ts-props/index.js [452:1034]


function getPropTypesForNode(node, optional, state) {
  const types = state.get('types');

  if (node.isAlreadyResolved === true) return node;

  let propType;
  switch (node.type) {
    // a type value by identifier
    case 'TSTypeReference':
      propType = resolveIdentifierToPropTypes(node, state);
      break;

    // a type annotation on a node
    // Array<Foo>
    //       ^^^ Foo
    case 'TSTypeAnnotation':
      propType = getPropTypesForNode(node.typeAnnotation, true, state);

      if (
        types.isLiteral(propType) ||
        (types.isIdentifier(propType) && propType.name === 'undefined')
      ) {
        // can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ])
        propType = convertLiteralToOneOf(types, propType);
      }
      break;

    // Foo['bar']
    case 'TSIndexedAccessType':
      // verify the type of index access
      if (types.isTSLiteralType(node.indexType) === false) break;

      const indexedName = node.indexType.literal.value;
      const objectPropType = getPropTypesForNode(node.objectType, true, state);

      // verify this came out as a PropTypes.shape(), which we can pick the indexed property off of
      if (
        types.isCallExpression(objectPropType) &&
        types.isMemberExpression(objectPropType.callee) &&
        types.isIdentifier(objectPropType.callee.object) &&
        objectPropType.callee.object.name === 'PropTypes' &&
        types.isIdentifier(objectPropType.callee.property) &&
        objectPropType.callee.property.name === 'shape'
      ) {
        const shapeProps = objectPropType.arguments[0].properties;
        for (let i = 0; i < shapeProps.length; i++) {
          const prop = shapeProps[i];
          if (prop.key.name === indexedName) {
            propType = makePropTypeOptional(types, prop.value);
            break;
          }
        }
      }

      break;

    // translates intersections (Foo & Bar & Baz) to a shape with the types' members (Foo, Bar, Baz) merged together
    case 'TSIntersectionType':
      const usableNodes = [...node.types].filter((node) => {
        const nodePropTypes = getPropTypesForNode(node, true, state);

        if (
          types.isMemberExpression(nodePropTypes) &&
          nodePropTypes.object.name === 'PropTypes' &&
          nodePropTypes.property.name === 'any'
        ) {
          return false;
        }

        // validate that this resulted in a shape or oneOfType, otherwise we don't know how to extract/merge the values
        if (
          !types.isCallExpression(nodePropTypes) ||
          !types.isMemberExpression(nodePropTypes.callee) ||
          nodePropTypes.callee.object.name !== 'PropTypes' ||
          (nodePropTypes.callee.property.name !== 'shape' &&
            nodePropTypes.callee.property.name !== 'oneOfType')
        ) {
          return false;
        }

        return true;
      });

      // merge the resolved proptypes for each intersection member into one object, mergedProperties
      const mergedProperties = usableNodes.reduce((mergedProperties, node) => {
        let nodePropTypes = getPropTypesForNode(node, true, state);

        // if this is a `oneOfType` extract those properties into a `shape`
        if (
          types.isCallExpression(nodePropTypes) &&
          types.isMemberExpression(nodePropTypes.callee) &&
          nodePropTypes.callee.object.name === 'PropTypes' &&
          nodePropTypes.callee.property.name === 'oneOfType'
        ) {
          const properties = nodePropTypes.arguments[0].elements
            .map((propType) => {
              // This exists on a oneOfType which must be expressed as an optional proptype
              propType = makePropTypeOptional(types, propType);

              // validate we're working with a shape, otherwise we can't merge properties
              if (
                !types.isCallExpression(propType) ||
                !types.isMemberExpression(propType.callee) ||
                propType.callee.object.name !== 'PropTypes' ||
                propType.callee.property.name !== 'shape'
              ) {
                return null;
              }

              // extract all of the properties from this group and make them optional
              return propType.arguments[0].properties.map((property) => {
                property.value = makePropTypeOptional(types, property.value);
                return property;
              });
            })
            .filter((x) => x !== null)
            .reduce((allProperties, properties) => {
              return [...allProperties, ...properties];
            }, []);

          nodePropTypes = types.callExpression(
            types.memberExpression(
              types.identifier('PropTypes'),
              types.identifier('shape')
            ),
            [types.objectExpression(properties)]
          );
        }

        // iterate over this type's members, adding them (and their comments) to `mergedProperties`
        const typeProperties = nodePropTypes.arguments[0].properties; // properties on the ObjectExpression passed to PropTypes.shape()
        for (let i = 0; i < typeProperties.length; i++) {
          const typeProperty = typeProperties[i];
          // this member may be duplicated between two shapes, e.g. Foo = { buzz: string } & Bar = { buzz: string }
          // either or both may have leading comments and we want to forward all comments to the generated prop type
          const leadingComments = [
            ...(typeProperty.leadingComments || []),
            ...((mergedProperties[typeProperty.key.name]
              ? mergedProperties[typeProperty.key.name].leadingComments
              : null) || []),
          ];

          let propTypeValue = typeProperty.value;
          if (
            types.isLiteral(propTypeValue) ||
            (types.isIdentifier(propTypeValue) &&
              propTypeValue.name === 'undefined')
          ) {
            // can't use a literal straight, wrap it with PropTypes.oneOf([ the_literal ])
            propTypeValue = convertLiteralToOneOf(types, propTypeValue);
          }

          // if this property has already been found, the only action is to potentially change it to optional
          if (mergedProperties.hasOwnProperty(typeProperty.key.name)) {
            const existing = mergedProperties[typeProperty.key.name];
            if (!areExpressionsIdentical(existing, typeProperty.value)) {
              mergedProperties[typeProperty.key.name] = types.callExpression(
                types.memberExpression(
                  types.identifier('PropTypes'),
                  types.identifier('oneOfType')
                ),
                [types.arrayExpression([existing, propTypeValue])]
              );

              if (
                isPropTypeRequired(types, existing) &&
                isPropTypeRequired(types, typeProperty.value)
              ) {
                mergedProperties[typeProperty.key.name] = makePropTypeRequired(
                  types,
                  mergedProperties[typeProperty.key.name]
                );
              }
            }
          } else {
            // property hasn't been seen yet, add it
            mergedProperties[typeProperty.key.name] = propTypeValue;
          }

          mergedProperties[typeProperty.key.name].leadingComments =
            leadingComments;
        }

        return mergedProperties;
      }, {});

      const propertyKeys = Object.keys(mergedProperties);
      // if there is one or more members on `mergedProperties` then use PropTypes.shape,
      // otherwise none of the types were resolvable and fallback to PropTypes.any
      if (propertyKeys.length > 0) {
        // At least one type/interface was resolved to proptypes
        propType = types.callExpression(
          types.memberExpression(
            types.identifier('PropTypes'),
            types.identifier('shape')
          ),
          [
            types.objectExpression(
              propertyKeys.map((propKey) => {
                const objectProperty = types.objectProperty(
                  types.identifier(propKey),
                  mergedProperties[propKey]
                );

                objectProperty.leadingComments =
                  mergedProperties[propKey].leadingComments;
                mergedProperties[propKey].leadingComments = null;

                return objectProperty;
              })
            ),
          ]
        );
      } else {
        // None of the types were resolveable, return with PropTypes.any
        propType = types.memberExpression(
          types.identifier('PropTypes'),
          types.identifier('any')
        );
      }
      break;

    // translate an interface definition into a PropTypes.shape
    case 'TSInterfaceBody':
      propType = types.callExpression(
        types.memberExpression(
          types.identifier('PropTypes'),
          types.identifier('shape')
        ),
        [
          types.objectExpression(
            node.body
              // This helps filter out index signatures from interfaces,
              // which don't translate to prop types.
              .filter(
                (property) =>
                  property.key != null &&
                  !types.isTSNeverKeyword(
                    property.typeAnnotation.typeAnnotation
                  )
              )
              .map((property) => {
                let propertyPropType =
                  property.type === 'TSMethodSignature'
                    ? getPropTypesForNode(
                        { type: 'TSFunctionType' },
                        property.optional,
                        state
                      )
                    : getPropTypesForNode(
                        property.typeAnnotation,
                        property.optional,
                        state
                      );

                if (
                  types.isLiteral(propertyPropType) ||
                  (types.isIdentifier(propertyPropType) &&
                    propertyPropType.name === 'undefined')
                ) {
                  propertyPropType = convertLiteralToOneOf(
                    types,
                    propertyPropType
                  );
                  if (!property.optional) {
                    propertyPropType = makePropTypeRequired(
                      types,
                      propertyPropType
                    );
                  }
                }

                const objectProperty = types.objectProperty(
                  types.identifier(
                    property.key.name || `"${property.key.value}"`
                  ),
                  propertyPropType
                );
                if (property.leadingComments != null) {
                  objectProperty.leadingComments = property.leadingComments.map(
                    ({ type, value }) => ({ type, value })
                  );
                }
                return objectProperty;
              })
          ),
        ]
      );
      break;

    // resolve a type operator (keyword) that operates on a value
    // currently only supporting `keyof typeof [object variable]`
    case 'TSTypeOperator':
      if (
        node.operator === 'keyof' &&
        node.typeAnnotation.type === 'TSTypeQuery'
      ) {
        const typeDefinitions = state.get('typeDefinitions');
        const typeDefinition =
          typeDefinitions[node.typeAnnotation.exprName.name];
        if (typeDefinition != null) {
          propType = getPropTypesForNode(typeDefinition, true, state);
        }
      }
      break;

    // invoked only by `keyof typeof` TSTypeOperator, safe to form PropTypes.oneOf(Object.keys(variable))
    case 'ObjectExpression':
      propType = types.callExpression(
        types.memberExpression(
          types.identifier('PropTypes'),
          types.identifier('oneOf')
        ),
        [
          types.arrayExpression(
            node.properties.map((property) =>
              types.stringLiteral(
                property.key
                  ? property.key.name || property.key.value
                  : property.argument.name
              )
            )
          ),
        ]
      );
      break;

    // translate a type definition into a PropTypes.shape
    case 'TSTypeLiteral':
      propType = types.callExpression(
        types.memberExpression(
          types.identifier('PropTypes'),
          types.identifier('shape')
        ),
        [
          types.objectExpression(
            node.members
              .map((property) => {
                // skip never keyword
                if (
                  types.isTSNeverKeyword(property.typeAnnotation.typeAnnotation)
                )
                  return null;
                // skip TS index signatures
                if (types.isTSIndexSignature(property)) return null;

                const propertyPropType =
                  property.type === 'TSMethodSignature'
                    ? getPropTypesForNode(
                        { type: 'TSFunctionType' },
                        property.optional,
                        state
                      )
                    : getPropTypesForNode(
                        property.typeAnnotation,
                        property.optional,
                        state
                      );

                const objectProperty = types.objectProperty(
                  types.identifier(
                    property.key.name || `"${property.key.value}"`
                  ),
                  propertyPropType
                );
                if (property.leadingComments != null) {
                  objectProperty.leadingComments = property.leadingComments.map(
                    ({ type, value }) => ({ type, value })
                  );
                }
                return objectProperty;
              })
              .filter((x) => x != null)
          ),
        ]
      );
      break;

    // translate a union (Foo | Bar | Baz) into PropTypes.oneOf or PropTypes.oneOfType, depending on
    // the kind of members in the union (no literal values, all literals, or mixed)
    // literal values are extracted into a `oneOf`, if all members are literals this oneOf is used
    // otherwise `oneOfType` is used - if there are any literal values it contains the literals' `oneOf`
    case 'TSUnionType':
      const tsUnionTypes = node.types.map((node) =>
        getPropTypesForNode(node, false, state)
      );

      // `tsUnionTypes` could be:
      // 1. all non-literal values (string | number)
      // 2. all literal values ("foo" | "bar")
      // 3. a mix of value types ("foo" | number)
      // this reduce finds any literal values and groups them into a oneOf node

      const reducedUnionTypes = tsUnionTypes.reduce(
        (foundTypes, tsUnionType) => {
          if (
            types.isLiteral(tsUnionType) ||
            (types.isIdentifier(tsUnionType) &&
              tsUnionType.name === 'undefined')
          ) {
            if (foundTypes.oneOfPropType == null) {
              foundTypes.oneOfPropType = types.arrayExpression([]);
              foundTypes.unionTypes.push(
                types.callExpression(
                  types.memberExpression(
                    types.identifier('PropTypes'),
                    types.identifier('oneOf')
                  ),
                  [foundTypes.oneOfPropType]
                )
              );
            }

            // this is a literal value, move to the oneOfPropType argument
            foundTypes.oneOfPropType.elements.push(tsUnionType);
          } else {
            // this is a non-literal type
            foundTypes.unionTypes.push(tsUnionType);
          }

          return foundTypes;
        },
        {
          unionTypes: [],
          oneOfPropType: null,
        }
      );

      // if there is only one member on the reduced union types, _and_a oneOf proptype was created,
      // then that oneOf proptype is the one member on union types, and can be be extracted out
      // e.g.
      //    PropTypes.oneOf([PropTypes.oneOf(['red', 'blue'])])
      //    ->
      //    PropTypes.oneOf(['red', 'blue'])
      if (
        reducedUnionTypes.unionTypes.length === 1 &&
        reducedUnionTypes.oneOfPropType != null
      ) {
        // the only proptype is a `oneOf`, use only that
        propType = reducedUnionTypes.unionTypes[0];
      } else {
        propType = types.callExpression(
          types.memberExpression(
            types.identifier('PropTypes'),
            types.identifier('oneOfType')
          ),
          [types.arrayExpression(reducedUnionTypes.unionTypes)]
        );
      }
      break;

    // translate enum to PropTypes.oneOf
    case 'TSEnumDeclaration':
      const memberTypes = node.members.map((member) =>
        getPropTypesForNode(member, true, state)
      );
      propType = types.callExpression(
        types.memberExpression(
          types.identifier('PropTypes'),
          types.identifier('oneOf')
        ),
        [types.arrayExpression(memberTypes)]
      );
      break;

    // translate an interface to PropTypes
    case 'TSInterfaceDeclaration':
      const { body, extends: extensions } = node;

      // if the interface doesn't extend anything use just the interface body
      if (extensions == null) {
        propType = getPropTypesForNode(body, true, state);
      } else {
        // fake a TSIntersectionType to merge everything together
        propType = getPropTypesForNode(
          {
            type: 'TSIntersectionType',
            types: [body, ...extensions],
          },
          true,
          state
        );
      }
      break;

    // simple pass-through wrapper
    case 'TSExpressionWithTypeArguments':
      propType = resolveIdentifierToPropTypes(node.expression, state);
      break;

    // an enum member is a simple wrapper around a type definition
    case 'TSEnumMember':
      propType = getPropTypesForNode(node.initializer, optional, state);
      break;

    // translate `string` to `PropTypes.string`
    case 'TSStringKeyword':
      propType = buildPropTypePrimitiveExpression(types, 'string');
      break;

    // translate `number` to `PropTypes.number`
    case 'TSNumberKeyword':
      propType = buildPropTypePrimitiveExpression(types, 'number');
      break;

    // translate `boolean` to `PropTypes.bool`
    case 'TSBooleanKeyword':
      propType = buildPropTypePrimitiveExpression(types, 'bool');
      break;

    // translate any function type to `PropTypes.func`
    case 'TSFunctionType':
      propType = buildPropTypePrimitiveExpression(types, 'func');
      break;

    // translate an array type, e.g. Foo[]
    case 'TSArrayType':
      propType = resolveArrayTypeToPropTypes(node, state);
      break;

    // parenthesized type is a small wrapper around another type definition
    // e.g. (() => void)[]
    //      ^^^^^^^^^^^^ wrapping the TSFunctionType `() => void`
    case 'TSParenthesizedType':
      propType = getPropTypesForNode(node.typeAnnotation, optional, state);
      optional = true; // handling `optional` has been delegated to the above call
      break;

    // literal type wraps a literal value
    case 'TSLiteralType':
      propType = getPropTypesForNode(node.literal, true, state);
      optional = true; // cannot call `.isRequired` on a literal
      break;

    case 'StringLiteral':
      propType = types.stringLiteral(node.value);
      optional = true; // cannot call `.isRequired` on a string literal
      break;

    case 'NumericLiteral':
      propType = types.numericLiteral(node.value);
      optional = true; // cannot call `.isRequired` on a number literal
      break;

    case 'BooleanLiteral':
      propType = types.booleanLiteral(node.value);
      optional = true; // cannot call `.isRequired` on a boolean literal
      break;

    case 'TSNullKeyword':
      propType = types.nullLiteral();
      optional = true; // cannot call `.isRequired` on a null literal
      break;

    case 'TSUndefinedKeyword':
      propType = types.identifier('undefined');
      optional = true; // cannot call `.isRequired` on an undefined literal
      break;

    // very helpful debugging code
    // default:
    //   debugger;
    //   throw new Error(`Could not generate prop types for node type ${node.type}`);
  }

  // if the node was unable to be translated to a prop type, fallback to PropTypes.any
  if (propType == null) {
    propType = types.memberExpression(
      types.identifier('PropTypes'),
      types.identifier('any')
    );
  }

  if (!optional) {
    propType = makePropTypeRequired(types, propType);
  }

  if (isTSType(node)) {
    propType.isTSType = true;
  }

  return propType;
}