export function modelType()

in compiler/src/model/utils.ts [78:368]


export function modelType (node: Node): model.ValueOf {
  switch (node.getKind()) {
    case ts.SyntaxKind.BooleanKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'boolean',
          namespace: '_builtins'
        }
      }
      return type
    }

    case ts.SyntaxKind.StringKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'string',
          namespace: '_builtins'
        }
      }
      return type
    }

    case ts.SyntaxKind.NumberKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'number',
          namespace: '_builtins'
        }
      }
      return type
    }

    case ts.SyntaxKind.NullKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'null',
          namespace: '_builtins'
        }
      }
      return type
    }

    case ts.SyntaxKind.VoidKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'void',
          namespace: '_builtins'
        }
      }
      return type
    }

    // TODO: this should not be used in the specification
    //       we should throw an error
    case ts.SyntaxKind.AnyKeyword: {
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name: 'object',
          namespace: '_builtins'
        }
      }
      return type
    }

    case ts.SyntaxKind.ArrayType: {
      const kinds: ts.SyntaxKind[] = [
        ts.SyntaxKind.StringKeyword,
        ts.SyntaxKind.BooleanKeyword,
        ts.SyntaxKind.AnyKeyword,
        ts.SyntaxKind.ArrayType,
        ts.SyntaxKind.TypeReference
      ]
      let children: Node[] = []
      node.forEachChild(child => children.push(child))
      children = children.filter(child => kinds.some(kind => kind === child.getKind()))
      assert(node, children.length === 1, `Expected array to have 1 usable child but saw ${children.length}`)

      const type: model.ArrayOf = {
        kind: 'array_of',
        value: modelType(children[0])
      }
      return type
    }

    case ts.SyntaxKind.UnionType: {
      assert(node, Node.isUnionTypeNode(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.UnionType]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const type: model.UnionOf = {
        kind: 'union_of',
        items: node.getTypeNodes().map(node => modelType(node))
      }
      return type
    }

    case ts.SyntaxKind.LiteralType: {
      assert(node, Node.isLiteralTypeNode(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.LiteralType]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      return modelType(node.getLiteral())
    }

    case ts.SyntaxKind.StringLiteral: {
      assert(node, Node.isStringLiteral(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.StringLiteral]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const type: model.LiteralValue = {
        kind: 'literal_value',
        value: node.getText().replace(/'/g, '')
      }
      return type
    }

    case ts.SyntaxKind.TrueKeyword: {
      assert(node, Node.isTrueLiteral(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.TrueKeyword]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const type: model.LiteralValue = {
        kind: 'literal_value',
        value: true
      }
      return type
    }

    case ts.SyntaxKind.FalseKeyword: {
      assert(node, Node.isFalseLiteral(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.FalseKeyword]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const type: model.LiteralValue = {
        kind: 'literal_value',
        value: false
      }
      return type
    }

    case ts.SyntaxKind.NumericLiteral: {
      assert(node, Node.isNumericLiteral(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.NumericLiteral]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const type: model.LiteralValue = {
        kind: 'literal_value',
        value: Number(node.getText())
      }
      return type
    }

    case ts.SyntaxKind.PrefixUnaryExpression: {
      // Negative number
      const type: model.LiteralValue = {
        kind: 'literal_value',
        value: Number(node.getText())
      }
      return type
    }

    case ts.SyntaxKind.TypeParameter: {
      assert(node, Node.isTypeParameterDeclaration(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.TypeReference]} but ${ts.SyntaxKind[node.getKind()]} instead`)
      const name = node.compilerNode.getText()
      const type: model.InstanceOf = {
        kind: 'instance_of',
        type: {
          name,
          namespace: getNameSpace(node)
        }
      }
      return type
    }
    case ts.SyntaxKind.TypeReference: {
      // TypeReferences are fun types, it's basically how TypeScript defines
      // everything that is not a basic type, an interface or a class will
      // appear here when used for instance.
      // For some reason also the `Array` type (the one defined with `Array<T>` and not `T[]`)
      // is interpreted as TypeReference as well.
      // The two most important fields of a TypeReference are `typeName` and `typeArguments`,
      // the first one is the name of the type (eg: `Foo`), while the second is the
      // possible generics (eg: Foo<T> => T will be in typeArguments).
      assert(node, Node.isTypeReference(node), `The node is not of type ${ts.SyntaxKind[ts.SyntaxKind.TypeReference]} but ${ts.SyntaxKind[node.getKind()]} instead`)

      const identifier = node.getTypeName()
      assert(node, Node.isIdentifier(identifier), 'Should be an identifier')

      const name = identifier.compilerNode.escapedText as string
      switch (name) {
        case 'Array': {
          assert(node, node.getTypeArguments().length === 1, 'An array must have one argument')
          const [value] = node.getTypeArguments().map(node => modelType(node))
          const type: model.ArrayOf = {
            kind: 'array_of',
            value
          }
          return type
        }

        case 'ArrayBuffer': {
          const type: model.InstanceOf = {
            kind: 'instance_of',
            type: {
              name: 'binary',
              namespace: '_builtins'
            }
          }
          return type
        }

        case 'Dictionary':
        case 'AdditionalProperties': {
          assert(node, node.getTypeArguments().length === 2, 'A Dictionary must have two arguments')
          const [key, value] = node.getTypeArguments().map(node => modelType(node))
          const type: model.DictionaryOf = {
            kind: 'dictionary_of',
            key,
            value,
            singleKey: false
          }
          return type
        }

        case 'SingleKeyDictionary':
        case 'AdditionalProperty': {
          assert(node, node.getTypeArguments().length === 2, 'A SingleKeyDictionary must have two arguments')
          const [key, value] = node.getTypeArguments().map(node => modelType(node))
          const type: model.DictionaryOf = {
            kind: 'dictionary_of',
            key,
            value,
            singleKey: true
          }
          return type
        }

        case 'UserDefinedValue': {
          const type: model.UserDefinedValue = {
            kind: 'user_defined_value'
          }
          return type
        }

        default: {
          const generics = node.getTypeArguments().map(node => modelType(node))
          const identifier = node.getTypeName()
          assert(node, Node.isIdentifier(identifier), 'Not an identifier')
          assert(node, identifier.getDefinitions().length > 0, 'Unknown definition (missing import?)')

          const declaration = identifier.getDefinitions()[0].getDeclarationNode()
          // We are looking at a generic parameter
          if (declaration == null) {
            const type: model.InstanceOf = {
              kind: 'instance_of',
              ...(generics.length > 0 && { generics }),
              type: {
                name,
                namespace: getNameSpace(node)
              }
            }
            return type
          }

          assert(
            node,
            Node.isClassDeclaration(declaration) ||
            Node.isInterfaceDeclaration(declaration) ||
            Node.isEnumDeclaration(declaration) ||
            Node.isTypeAliasDeclaration(declaration) ||
            Node.isTypeParameterDeclaration(declaration),
            'It should be a class, interface, enum, type alias, or type parameter declaration'
          )
          const type: model.InstanceOf = {
            kind: 'instance_of',
            ...(generics.length > 0 && { generics }),
            type: {
              name: declaration.getName() as string,
              namespace: getNameSpace(node)
            }
          }

          if (Node.isTypeParameterDeclaration(declaration)) {
            const parent = declaration.getParent()
            assert(
              parent,
              Node.isClassDeclaration(parent) ||
              Node.isInterfaceDeclaration(parent) ||
              Node.isTypeAliasDeclaration(parent),
              'It should be a class, interface, or type alias declaration'
            )

            type.type.namespace = `${type.type.namespace}.${parent.getName() as string}`
          }

          return type
        }
      }
    }

    default:
      assert(node, false, `Unhandled node kind: ${ts.SyntaxKind[node.getKind()]}`)
  }
}