File

libs/nx/src/executors/introspection/compat/utils/extract-form-properties.ts

Index

Properties

Properties

pathFromRoot
Type string[]
property
Type ts.Symbol
visitedCustomTypeNamesFromRoot
Type Set<string>
import { Logger } from '@angular-devkit/core/src/logger';
import path from 'path';
import ts, { NodeWithTypeArguments, TypeFlags, TypeNode, UnionTypeNode } from 'typescript';
import { IntrospectionContext } from '../../../shared/introspection/types';

export type FormProperty = {
  name: string;
  type: string;
};

export interface FormPropertyPreprocessing {
  property: ts.Symbol;
  pathFromRoot: string[];
  visitedCustomTypeNamesFromRoot: Set<string>;
}

export function extractFormProperties(
  introspectionContext: IntrospectionContext,
  sourceFile: ts.SourceFile,
  node: ts.Node | undefined
) {
  const { checker, logger } = introspectionContext;
  const buildingBlockFilePath = sourceFile?.fileName;
  const buildingBlockName = path.basename(buildingBlockFilePath, '.ts');

  if (!node) return [];

  let result: FormProperty[] = [];

  node.forEachChild((visitingNode: ts.Node) => {
    if (isGetFormMethod(visitingNode)) {
      const declaration = visitingNode as ts.MethodDeclaration;
      const type = getReturnTypeArgument(checker, declaration);

      if (type?.flags === TypeFlags.Any) {
        /**
         * @deprecated This will turn into an error in the future
         */
        logger.warn(
          `Can't extract the return type of 'getForm' method in '${buildingBlockName}'. Make sure the form is not explicitly typed with 'UntypedFormGroup' or uses a 'FormGroup' without a generic type. This will turn into an error in the future.`
        );
      } else if (type) {
        result = extractPropertyTree(checker, type, visitingNode, buildingBlockFilePath, logger);
      } else {
        logger.error(
          `The return type of 'getForm' method in '${buildingBlockName}' is not proper. Make sure the form is properly typed with the 'FormGroupTyped<T>' and the return type of 'getForm' method matches this signature!`
        );
      }
    }
  });

  return result;
}

function isGetFormMethod(node: ts.Node) {
  const isMethod = ts.isMethodDeclaration(node);
  if (!isMethod) return false;
  const declaration = node as ts.MethodDeclaration;
  const name = declaration.name as ts.Identifier;
  return name?.text === 'getForm';
}

function getReturnTypeArgument(checker: ts.TypeChecker, declaration: ts.MethodDeclaration) {
  const methodSignature = checker.getSignatureFromDeclaration(declaration);

  if (!methodSignature) return;

  const returnType = checker.getReturnTypeOfSignature(methodSignature) as ts.TypeReference;
  // FormGroupTyped<T,U> , we need to get the T which is the first item in the 'typeArguments'
  const typeArgument = returnType['typeArguments']?.[0];
  return typeArgument;
}

function extractTypeName(propertyType: ts.Type): string {
  return (
    propertyType?.aliasSymbol?.escapedName ?? // If it is a Type , use it
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (propertyType as any)['intrinsicName'] ?? // If it is an Interface or a custom type, use other
    propertyType.getSymbol()?.escapedName.toString()
  );
}

function extractFormControlType(
  propertyType: NodeWithTypeArguments,
  pathFromRoot: string[],
  logger: Logger
): string {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const typeArguments: any | undefined = propertyType.typeArguments?.[0];

  if (typeArguments?.intrinsicName) {
    return typeArguments.intrinsicName;
  }

  // In some cases the type information is not available. This is most likely related to Typescript's strict mode being off
  if (!(typeArguments as UnionTypeNode).types) {
    /**
     * @deprecated This will turn into an error in the future
     */
    logger.warn(
      `Could not extract type information for form element: ${pathFromRoot.join(
        '.'
      )}. This might be related to strict mode being turned off.`
    );
    return '';
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (typeArguments as UnionTypeNode).types.map((type: any) => type.intrinsicName).join(' | ');
}

function extractFormControlProperty(
  propertyType: NodeWithTypeArguments,
  pathFromRoot: string[],
  logger: Logger
): FormProperty[] {
  return [
    {
      name: pathFromRoot.join('.'),
      type: extractFormControlType(propertyType, pathFromRoot, logger)
    }
  ];
}

function extractFormGroupProperties(
  typeArguments: TypeNode,
  pathFromRoot: string[],
  logger: Logger
): FormProperty[] {
  const formProperties: FormProperty[] = [];

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  if ((typeArguments as any)?.properties?.length) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (typeArguments as any).properties.forEach((property: ts.Symbol) => {
      const currentPath = [...pathFromRoot, property.escapedName as string];
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const type = (property as any).links.type;

      if (type) {
        formProperties.push(
          ...extractAngularFormProperties(type, currentPath, logger, type.symbol.escapedName)
        );
      } else {
        logger.error(`Could not extract type information for ${currentPath}`);
      }
    });
  }

  return formProperties;
}

function extractAngularFormProperties(
  propertyType: NodeWithTypeArguments,
  pathFromRoot: string[],
  logger: Logger,
  typeName?: string
): FormProperty[] {
  const typeArguments: TypeNode | undefined = propertyType.typeArguments?.[0];

  if (!typeArguments) {
    return [];
  }

  if (typeName === 'FormArray') {
    return extractAngularFormProperties(
      typeArguments as NodeWithTypeArguments,
      pathFromRoot,
      logger,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (typeArguments as any).symbol.escapedName
    );
  }

  if (typeName === 'FormGroup') {
    return extractFormGroupProperties(typeArguments, pathFromRoot, logger);
  }

  if (typeName === 'FormRecord') {
    return [
      {
        name: pathFromRoot.join('.'),
        type: 'FormRecord'
      }
    ];
  }

  if (typeName === 'FormControl') {
    return extractFormControlProperty(
      propertyType as unknown as NodeWithTypeArguments,
      pathFromRoot,
      logger
    );
  }

  return [];
}

function isAngularForms(typeName: string) {
  return typeName === 'FormArray' || typeName === 'FormGroup' || typeName === 'FormControl';
}

function extractPropertyTree(
  checker: ts.TypeChecker,
  root: ts.Type,
  node: ts.Node,
  buildingBlockFilePath: string,
  logger: Logger
) {
  const formInterfaceName = root.getSymbol()?.escapedName;
  const formProperties: FormProperty[] = [];
  const propertyStack = mapToStackItems(new Set<string>(), root, []);

  while (propertyStack.length > 0) {
    const { property, pathFromRoot, visitedCustomTypeNamesFromRoot } =
      propertyStack.shift() as FormPropertyPreprocessing;
    const { propertyType, isArrayType } = getPropertyTypeInformation(
      checker,
      property,
      node,
      logger
    );

    // Add array signature if it is an array
    if (isArrayType) pathFromRoot.push('[]');

    const typeName = extractTypeName(propertyType);

    if (typeName === 'UntypedFormGroup' || typeName === 'UntypedFormControl') {
      /**
       * @deprecated This will turn into an error in the future
       */
      logger.warn(
        `Can't extract the type '${typeName}' of the path '${pathFromRoot}' in the form in '${buildingBlockFilePath}'. Make sure the form is not explicitly typed with 'UntypedFormGroup' or 'UntypedFormControl'. This will turn into an error in the future.`
      );
      continue;
    }

    if (isAngularForms(typeName)) {
      formProperties.push(
        ...extractAngularFormProperties(
          propertyType as unknown as NodeWithTypeArguments,
          pathFromRoot,
          logger,
          typeName
        )
      );
      continue;
    }

    const isCustomType = isCustomDefinedType(propertyType);

    // If it is not a custom type, we just add it to the result
    if (!isCustomType) {
      formProperties.push({
        name: pathFromRoot.join('.'),
        type: isArrayType ? `${typeName}[]` : typeName
      });
      // Go to the next property
      continue;
    }

    // If it is a custom type, we need to check if it has recursion
    const isRecursionDetected =
      typeName && // If it has a name
      property.escapedName !== 'prototype' && // If it is not class prototype
      visitedCustomTypeNamesFromRoot.has(typeName); // If it is already visited in the current branch

    if (isRecursionDetected) {
      const symbolFilePath = propertyType
        .getSymbol()
        ?.getDeclarations()?.[0]
        .getSourceFile().fileName;

      logger.error(`
        Recursion detected in the Form type '${formInterfaceName}'.
        Building Block file: '${buildingBlockFilePath}'.
        Recursion detected in '${typeName}' type defined in '${symbolFilePath}'
        Recursive route: '${pathFromRoot.join('.')}'
        `);
      continue;
    }

    const isCustomTypeWithName =
      propertyType?.aliasSymbol?.escapedName || // Do we have a name for the type?
      propertyType.getSymbol()?.flags !== ts.SymbolFlags.TypeLiteral; // Ignore TypeLiteral (object declaration) with name

    if (isCustomTypeWithName) {
      visitedCustomTypeNamesFromRoot.add(typeName); // Add the type to the visited list
    }
    propertyStack.push(
      ...mapToStackItems(visitedCustomTypeNamesFromRoot, propertyType, pathFromRoot) // Add the properties of the type to the stack
    );
  }
  return formProperties;
}

function mapToStackItems(visitedCustomTypeNames: Set<string>, type: ts.Type, ancestors: string[]) {
  return type.getProperties().map((property) => ({
    property,
    pathFromRoot: [...ancestors, property?.escapedName.toString()],
    visitedCustomTypeNamesFromRoot: new Set(visitedCustomTypeNames)
  }));
}

function getPropertyTypeInformation(
  checker: ts.TypeChecker,
  property: ts.Symbol,
  node: ts.Node,
  logger: Logger
) {
  const primitiveOrComplexType = checker.getTypeOfSymbolAtLocation(property, node);
  const isArrayType = primitiveOrComplexType.getSymbol()?.escapedName === 'Array';
  let propertyType: ts.Type;

  if (primitiveOrComplexType?.symbol?.escapedName === 'FormGroupTyped') {
    /**
     * @deprecated This will turn into an error in the future
     */
    logger.warn(
      `Detected a FormGroupTyped. Please notice that this type is deprecated and will be removed in the future. This will turn into an error in the future.`
    );
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    propertyType = (primitiveOrComplexType as any).typeArguments[0];
  } else if (isArrayType) {
    // If type is 'Person[]', extract 'Person' from type arguments
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    propertyType = (primitiveOrComplexType as any).typeArguments[0];
  } else {
    propertyType = primitiveOrComplexType;
  }

  return {
    isArrayType,
    propertyType
  };
}

function isCustomDefinedType(type: ts.Type) {
  return type
    .getSymbol()
    ?.getDeclarations()
    ?.some((d) => {
      return !d.getSourceFile().fileName.startsWith('node_modules/typescript/lib');
    });
}

results matching ""

    No results matching ""