nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
390 lines (364 loc) • 13.8 kB
text/typescript
import { ts, Type as TSMorphType, type Signature } from 'ts-morph'
import type { Type } from './types/Type.js'
import { NullType } from './types/NullType.js'
import { BooleanType } from './types/BooleanType.js'
import { NumberType } from './types/NumberType.js'
import { StringType } from './types/StringType.js'
import { BigIntType } from './types/BigIntType.js'
import { VoidType } from './types/VoidType.js'
import { ArrayType } from './types/ArrayType.js'
import { FunctionType } from './types/FunctionType.js'
import { PromiseType } from './types/PromiseType.js'
import { RecordType } from './types/RecordType.js'
import { ArrayBufferType } from './types/ArrayBufferType.js'
import { EnumType } from './types/EnumType.js'
import { HybridObjectType } from './types/HybridObjectType.js'
import { StructType } from './types/StructType.js'
import { OptionalType } from './types/OptionalType.js'
import { NamedWrappingType } from './types/NamedWrappingType.js'
import { getInterfaceProperties } from './getInterfaceProperties.js'
import { VariantType } from './types/VariantType.js'
import { MapType } from './types/MapType.js'
import { TupleType } from './types/TupleType.js'
import {
isAnyHybridSubclass,
isDirectlyHybridObject,
isHybridView,
type Language,
} from '../getPlatformSpecs.js'
import { HybridObjectBaseType } from './types/HybridObjectBaseType.js'
import { ErrorType } from './types/ErrorType.js'
import { getBaseTypes } from '../utils.js'
import { DateType } from './types/DateType.js'
function isSymbol(type: TSMorphType, symbolName: string): boolean {
const symbol = type.getSymbol()
if (symbol?.getName() === symbolName) {
return true
}
const aliasSymbol = type.getAliasSymbol()
if (aliasSymbol?.getName() === symbolName) {
return true
}
return false
}
function isPromise(type: TSMorphType): boolean {
return isSymbol(type, 'Promise')
}
function isRecord(type: TSMorphType): boolean {
return isSymbol(type, 'Record')
}
function isArrayBuffer(type: TSMorphType): boolean {
return isSymbol(type, 'ArrayBuffer')
}
function isDate(type: TSMorphType): boolean {
return isSymbol(type, 'Date')
}
function isMap(type: TSMorphType): boolean {
return isSymbol(type, 'AnyMap')
}
function isError(type: TSMorphType): boolean {
return isSymbol(type, 'Error')
}
function getHybridObjectName(type: TSMorphType): string {
const symbol = isHybridView(type) ? type.getAliasSymbol() : type.getSymbol()
if (symbol == null) {
throw new Error(
`Cannot get name of \`${type.getText()}\` - symbol not found!`
)
}
return symbol.getEscapedName()
}
function getFunctionCallSignature(func: TSMorphType): Signature {
const callSignatures = func.getCallSignatures()
const callSignature = callSignatures[0]
if (callSignatures.length !== 1 || callSignature == null) {
throw new Error(
`Function overloads are not supported in Nitrogen! (in ${func.getText()})`
)
}
return callSignature
}
function removeDuplicates(types: Type[]): Type[] {
return types.filter((t1, index, array) => {
const firstIndexOfType = array.findIndex(
(t2) => t1.getCode('c++') === t2.getCode('c++')
)
return firstIndexOfType === index
})
}
type Tuple<
T,
N extends number,
R extends unknown[] = [],
> = R['length'] extends N ? R : Tuple<T, N, [T, ...R]>
function getArguments<N extends number>(
type: TSMorphType,
typename: string,
count: N
): Tuple<TSMorphType<ts.Type>, N> {
const typeArguments = type.getTypeArguments()
const aliasTypeArguments = type.getAliasTypeArguments()
if (typeArguments.length === count) {
return typeArguments as Tuple<TSMorphType<ts.Type>, N>
}
if (aliasTypeArguments.length === count) {
return aliasTypeArguments as Tuple<TSMorphType<ts.Type>, N>
}
throw new Error(
`Type ${type.getText()} looks like a ${typename}, but has ${typeArguments.length} type arguments instead of ${count}!`
)
}
export function createNamedType(
language: Language,
name: string,
type: TSMorphType,
isOptional: boolean
) {
if (name.startsWith('__')) {
throw new Error(
`Name cannot start with two underscores (__) as this is reserved syntax for Nitrogen! (In ${type.getText()})`
)
}
return new NamedWrappingType(name, createType(language, type, isOptional))
}
export function createVoidType(): Type {
return new VoidType()
}
// Caches complex types (types that have a symbol)
type TypeId = string
const knownTypes: Record<Language, Map<TypeId, Type>> = {
'c++': new Map<TypeId, Type>(),
'swift': new Map<TypeId, Type>(),
'kotlin': new Map<TypeId, Type>(),
}
/**
* Get a list of all currently known complex types.
*/
export function getAllKnownTypes(language?: Language): Type[] {
if (language != null) {
// Get types for the given language
return Array.from(knownTypes[language].values())
} else {
// Get types for all languages alltogether
const allMaps = Object.values(knownTypes)
return allMaps.flatMap((m) => Array.from(m.values()))
}
}
function getTypeId(type: TSMorphType, isOptional: boolean): string {
const symbol = type.getSymbol()
let key = type.getText()
if (symbol != null) {
key += '_' + symbol.getFullyQualifiedName()
}
if (isOptional) key += '?'
return key
}
export function addKnownType(
key: string,
type: Type,
language: Language
): void {
if (knownTypes[language].has(key)) {
// type is already known
return
}
knownTypes[language].set(key, type)
}
function isSyncFunction(type: TSMorphType): boolean {
if (type.getCallSignatures().length < 1)
// not a function.
return false
const syncTag = type.getProperty('__syncTag')
return syncTag != null
}
/**
* Create a new type (or return it from cache if it is already known)
*/
export function createType(
language: Language,
type: TSMorphType,
isOptional: boolean
): Type {
const key = getTypeId(type, isOptional)
if (key != null && knownTypes[language].has(key)) {
const known = knownTypes[language].get(key)!
if (isOptional === known instanceof OptionalType) {
return known
}
}
const get = () => {
if (isOptional) {
const wrapping = createType(language, type, false)
return new OptionalType(wrapping)
}
if (type.isNull() || type.isUndefined()) {
return new NullType()
} else if (type.isBoolean() || type.isBooleanLiteral()) {
return new BooleanType()
} else if (type.isNumber() || type.isNumberLiteral()) {
if (type.isEnumLiteral()) {
// enum literals are technically just numbers - but we treat them differently in C++.
return createType(language, type.getBaseTypeOfLiteralType(), isOptional)
}
return new NumberType()
} else if (type.isString()) {
return new StringType()
} else if (type.isBigInt() || type.isBigIntLiteral()) {
return new BigIntType()
} else if (type.isVoid()) {
return new VoidType()
} else if (type.isArray()) {
const arrayElementType = type.getArrayElementTypeOrThrow()
const elementType = createType(language, arrayElementType, false)
return new ArrayType(elementType)
} else if (type.isTuple()) {
const itemTypes = type
.getTupleElements()
.map((t) => createType(language, t, t.isNullable()))
return new TupleType(itemTypes)
} else if (type.getCallSignatures().length > 0) {
// It's a function!
const callSignature = getFunctionCallSignature(type)
const funcReturnType = callSignature.getReturnType()
const returnType = createType(
language,
funcReturnType,
funcReturnType.isNullable()
)
const parameters = callSignature.getParameters().map((p) => {
const declaration = p.getValueDeclarationOrThrow()
const parameterType = p.getTypeAtLocation(declaration)
const isNullable = parameterType.isNullable() || p.isOptional()
return createNamedType(language, p.getName(), parameterType, isNullable)
})
const isSync = isSyncFunction(type)
return new FunctionType(returnType, parameters, isSync)
} else if (isPromise(type)) {
// It's a Promise!
const [promiseResolvingType] = getArguments(type, 'Promise', 1)
const resolvingType = createType(
language,
promiseResolvingType,
promiseResolvingType.isNullable()
)
return new PromiseType(resolvingType)
} else if (isRecord(type)) {
// Record<K, V> -> unordered_map<K, V>
const [keyTypeT, valueTypeT] = getArguments(type, 'Record', 2)
const keyType = createType(language, keyTypeT, false)
const valueType = createType(language, valueTypeT, false)
return new RecordType(keyType, valueType)
} else if (isArrayBuffer(type)) {
// ArrayBuffer
return new ArrayBufferType()
} else if (isMap(type)) {
// Map
return new MapType()
} else if (isDate(type)) {
// Date
return new DateType()
} else if (isError(type)) {
// Error
return new ErrorType()
} else if (type.isEnum()) {
// It is an enum. We need to generate a C++ declaration for the enum
const typename = type.getSymbolOrThrow().getEscapedName()
const declaration = type.getSymbolOrThrow().getValueDeclarationOrThrow()
const enumDeclaration = declaration.asKindOrThrow(
ts.SyntaxKind.EnumDeclaration
)
return new EnumType(typename, enumDeclaration)
} else if (type.isUnion()) {
// It is some kind of union;
// - of string literals (then it's an enum)
// - of type `T | undefined` (then it's just optional `T`)
// - of different types (then it's a variant `A | B | C`)
const types = type.getUnionTypes()
const nonNullTypes = types.filter(
(t) => !t.isNull() && !t.isUndefined() && !t.isVoid()
)
const isEnumUnion = nonNullTypes.every((t) => t.isStringLiteral())
if (isEnumUnion) {
// It consists only of string literaly - that means it's describing an enum!
const symbol = type.getNonNullableType().getAliasSymbol()
if (symbol == null) {
// If there is no alias, it is an inline union instead of a separate type declaration!
throw new Error(
`Inline union types ("${type.getText()}") are not supported by Nitrogen!\n` +
`Extract the union to a separate type, and re-run nitrogen!`
)
}
const typename = symbol.getEscapedName()
return new EnumType(typename, type)
} else {
// It consists of different types - that means it's a variant!
let variants = type
.getUnionTypes()
// Filter out any nulls or undefineds, as those are already treated as `isOptional`.
.filter((t) => !t.isNull() && !t.isUndefined() && !t.isVoid())
.map((t) => createType(language, t, false))
variants = removeDuplicates(variants)
if (variants.length === 1) {
// It's just one type with undefined/null variant(s) - so we treat it like a simple optional.
return variants[0]!
}
const name = type.getAliasSymbol()?.getName()
return new VariantType(variants, name)
}
} else if (isAnyHybridSubclass(type)) {
// It is another HybridObject being referenced!
const typename = getHybridObjectName(type)
const baseTypes = getBaseTypes(type)
.filter((t) => isAnyHybridSubclass(t))
.map((b) => createType(language, b, false))
const baseHybrids = baseTypes.filter((b) => b instanceof HybridObjectType)
return new HybridObjectType(typename, language, baseHybrids)
} else if (isDirectlyHybridObject(type)) {
// It is a HybridObject directly/literally. Base type
return new HybridObjectBaseType()
} else if (type.isInterface()) {
// It is an `interface T { ... }`, which is a `struct`
const typename = type.getSymbolOrThrow().getName()
const properties = getInterfaceProperties(language, type)
return new StructType(typename, properties)
} else if (type.isObject()) {
// It is an object. If it has a symbol/name, it is a `type T = ...` declaration, so a `struct`.
// Otherwise, it is an anonymous/inline object, which cannot be represented in native.
const symbol = type.getAliasSymbol()
if (symbol != null) {
// it has a `type T = ...` declaration
const typename = symbol.getName()
const properties = getInterfaceProperties(language, type)
return new StructType(typename, properties)
} else {
// It's an anonymous object (`{ ... }`)
throw new Error(
`Anonymous objects cannot be represented in C++! Extract "${type.getText()}" to a separate interface/type declaration.`
)
}
} else if (type.isStringLiteral()) {
throw new Error(
`String literal ${type.getText()} cannot be represented in C++ because it is ambiguous between a string and a discriminating union enum.`
)
} else {
if (type.getSymbol() == null) {
// There is no declaration for it!
// Could be an invalid import, e.g. an alias
throw new Error(
`The TypeScript type "${type.getText()}" cannot be resolved - is it imported properly? ` +
`Make sure to import it properly using fully specified relative or absolute imports, no aliases.`
)
} else {
// A different error
throw new Error(
`The TypeScript type "${type.getText()}" cannot be represented in C++!`
)
}
}
}
const result = get()
if (key != null) {
knownTypes[language].set(key, result)
}
return result
}