export function getDescriptor()

in packages/osd-telemetry-tools/src/tools/serializer.ts [127:272]


export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | DescriptorValue {
  if (ts.isMethodSignature(node) || ts.isPropertySignature(node)) {
    if (node.type) {
      return getDescriptor(node.type, program);
    }
  }
  if (ts.isTypeLiteralNode(node) || ts.isInterfaceDeclaration(node)) {
    return node.members.reduce((acc, m) => {
      const key = m.name?.getText();
      if (key) {
        return { ...acc, [key]: getDescriptor(m, program) };
      } else {
        return { ...acc, ...getDescriptor(m, program) };
      }
    }, {});
  }

  // If it's defined as signature { [key: string]: OtherInterface }
  if ((ts.isIndexSignatureDeclaration(node) || ts.isMappedTypeNode(node)) && node.type) {
    const descriptor = getDescriptor(node.type, program);

    // If we know the constraints of `string` ({ [key in 'prop1' | 'prop2']: value })
    const constraint = (node as ts.MappedTypeNode).typeParameter?.constraint;
    if (constraint) {
      const constraints = getConstraints(constraint, program);
      const constraintsArray = Array.isArray(constraints) ? constraints : [constraints];
      if (typeof constraintsArray[0] === 'string') {
        return constraintsArray.reduce((acc, c) => ({ ...acc, [c]: descriptor }), {});
      }
    }
    return { '@@INDEX@@': descriptor };
  }

  if (ts.SyntaxKind.FirstNode === node.kind) {
    return getDescriptor((node as any).right, program);
  }

  if (ts.isIdentifier(node)) {
    const identifierName = node.getText();
    if (identifierName === 'Date') {
      return { kind: TelemetryKinds.Date, type: 'Date' };
    }
    if (identifierName === 'Moment') {
      return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
    }
    throw new Error(`Unsupported Identifier ${identifierName}.`);
  }

  if (ts.isTypeReferenceNode(node)) {
    const typeChecker = program.getTypeChecker();
    const symbol = typeChecker.getSymbolAtLocation(node.typeName);
    const symbolName = symbol?.getName();
    if (symbolName === 'Moment') {
      return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
    }
    if (symbolName === 'Date') {
      return { kind: TelemetryKinds.Date, type: 'Date' };
    }
    // Support `Record<string, SOMETHING>`
    if (symbolName === 'Record') {
      const descriptor = getDescriptor(node.typeArguments![1], program);
      if (node.typeArguments![0].kind === ts.SyntaxKind.StringKeyword) {
        return { '@@INDEX@@': descriptor };
      }
      const constraints = getConstraints(node.typeArguments![0], program);
      const constraintsArray = Array.isArray(constraints) ? constraints : [constraints];
      if (typeof constraintsArray[0] === 'string') {
        return constraintsArray.reduce((acc, c) => ({ ...acc, [c]: descriptor }), {});
      }
    }

    // Support `Pick<SOMETHING, 'prop1' | 'prop2'>`
    if (symbolName === 'Pick') {
      const parentDescriptor = getDescriptor(node.typeArguments![0], program);
      const pickPropNames = getConstraints(node.typeArguments![1], program);
      return pick(parentDescriptor, pickPropNames);
    }

    const declaration = (symbol?.getDeclarations() || [])[0];
    if (declaration) {
      return getDescriptor(declaration, program);
    }
    return getDescriptor(node.typeName, program);
  }

  if (ts.isImportSpecifier(node)) {
    const source = node.getSourceFile();
    const importedModuleName = getModuleSpecifier(node);

    const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
    const declarationNode = getIdentifierDeclarationFromSource(node.name, declarationSource);
    return getDescriptor(declarationNode, program);
  }

  if (ts.isArrayTypeNode(node)) {
    return { items: getDescriptor(node.elementType, program) };
  }

  if (ts.isLiteralTypeNode(node)) {
    return {
      kind: node.literal.kind,
      type: ts.SyntaxKind[node.literal.kind] as keyof typeof ts.SyntaxKind,
    };
  }

  if (ts.isUnionTypeNode(node)) {
    const types = node.types.filter(discardNullOrUndefined);

    const kinds = types
      .map((typeNode) => getDescriptor(typeNode, program))
      .filter(discardNullOrUndefined);

    const uniqueKinds = uniqBy(kinds, 'kind');

    if (uniqueKinds.length !== 1) {
      throw Error('Mapping does not support conflicting union types.');
    }

    return uniqueKinds[0];
  }

  // Support `type MyUsageType = SomethingElse`
  if (ts.isTypeAliasDeclaration(node)) {
    return getDescriptor(node.type, program);
  }

  // Support `&` unions
  if (ts.isIntersectionTypeNode(node)) {
    return node.types.reduce(
      (acc, unionNode) => ({ ...acc, ...getDescriptor(unionNode, program) }),
      {}
    );
  }

  switch (node.kind) {
    case ts.SyntaxKind.NumberKeyword:
    case ts.SyntaxKind.BooleanKeyword:
    case ts.SyntaxKind.StringKeyword:
    case ts.SyntaxKind.SetKeyword:
      return { kind: node.kind, type: ts.SyntaxKind[node.kind] as keyof typeof ts.SyntaxKind };
    case ts.SyntaxKind.UnionType:
    case ts.SyntaxKind.AnyKeyword:
    default:
      throw new Error(`Unknown type ${ts.SyntaxKind[node.kind]}; ${node.getText()}`);
  }
}