nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
330 lines (329 loc) • 14.1 kB
JavaScript
import { ts, Type as TSMorphType } from 'ts-morph';
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, } 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, symbolName) {
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) {
return isSymbol(type, 'Promise');
}
function isRecord(type) {
return isSymbol(type, 'Record');
}
function isArrayBuffer(type) {
return isSymbol(type, 'ArrayBuffer');
}
function isDate(type) {
return isSymbol(type, 'Date');
}
function isMap(type) {
return isSymbol(type, 'AnyMap');
}
function isError(type) {
return isSymbol(type, 'Error');
}
function getHybridObjectName(type) {
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) {
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) {
return types.filter((t1, index, array) => {
const firstIndexOfType = array.findIndex((t2) => t1.getCode('c++') === t2.getCode('c++'));
return firstIndexOfType === index;
});
}
function getArguments(type, typename, count) {
const typeArguments = type.getTypeArguments();
const aliasTypeArguments = type.getAliasTypeArguments();
if (typeArguments.length === count) {
return typeArguments;
}
if (aliasTypeArguments.length === count) {
return aliasTypeArguments;
}
throw new Error(`Type ${type.getText()} looks like a ${typename}, but has ${typeArguments.length} type arguments instead of ${count}!`);
}
export function createNamedType(language, name, type, isOptional) {
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() {
return new VoidType();
}
const knownTypes = {
'c++': new Map(),
'swift': new Map(),
'kotlin': new Map(),
};
/**
* Get a list of all currently known complex types.
*/
export function getAllKnownTypes(language) {
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, isOptional) {
const symbol = type.getSymbol();
let key = type.getText();
if (symbol != null) {
key += '_' + symbol.getFullyQualifiedName();
}
if (isOptional)
key += '?';
return key;
}
export function addKnownType(key, type, language) {
if (knownTypes[language].has(key)) {
// type is already known
return;
}
knownTypes[language].set(key, type);
}
function isSyncFunction(type) {
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, type, isOptional) {
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;
}