UNPKG

nitro-codegen

Version:

The code-generator for react-native-nitro-modules.

390 lines (364 loc) 13.8 kB
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 }