mtcute/packages/tl-utils/src/parse.ts

281 lines
7.9 KiB
TypeScript
Raw Normal View History

import type { TlArgument, TlEntry } from './types.js'
import { computeConstructorIdFromEntry } from './ctor-id.js'
import { TL_PRIMITIVES } from './types.js'
import { parseArgumentType, parseTdlibStyleComment } from './utils.js'
// eslint-disable-next-line regexp/no-super-linear-backtracking
const SINGLE_REGEX = /^(.+?)(?:#([0-9a-f]{1,8}))?(?: \?)?(?: \{(.+?:.+?)\})? ((?:.+? )*)= (.+);$/
export function computeConstructorIdFromString(line: string): number {
return computeConstructorIdFromEntry(parseTlToEntries(line, { forIdComputation: true })[0])
}
2022-08-29 14:33:11 +03:00
/**
* Parse TL schema into a list of entries.
*
* @param tl TL schema
* @param params Additional parameters
*/
export function parseTlToEntries(
tl: string,
params: {
2022-08-29 14:33:11 +03:00
/**
* Whether to throw an error if a line failed to parse
*/
panicOnError?: boolean
2022-08-29 14:33:11 +03:00
/**
* Function to be called if there was an error while parsing a line
*
* @param err Error
* @param line Line that failed to parse
* @param num Line number
*/
onError?: (err: Error, line: string, num: number) => void
2022-08-29 14:33:11 +03:00
/**
* Function to be called a comment is found not belonging to any entry
*
* @param comment Comment text
*/
onOrphanComment?: (comment: string) => void
2022-08-29 14:33:11 +03:00
/**
* Prefix to be applied to all types
*/
prefix?: string
2022-08-29 14:33:11 +03:00
/**
* Whether this invocation is for computing constructor ids.
* If true, the `id` field will be set to 0 for all entries.
2022-08-29 14:33:11 +03:00
*/
forIdComputation?: boolean
/**
* Whether to parse typeModifiers for method return types
*/
parseMethodTypes?: boolean
} = {},
): TlEntry[] {
const ret: TlEntry[] = []
const entries: Record<string, TlEntry> = {}
const unions: Record<string, TlEntry[]> = {}
const lines = tl.split('\n')
let currentKind: TlEntry['kind'] = 'class'
let currentComment = ''
const prefix = params.prefix ?? ''
2023-11-09 21:03:52 +03:00
const handleError = (err: Error, entryIdx: number) => {
if (params.panicOnError) {
throw err
} else if (params.onError) {
params.onError(err, '', entryIdx)
/* c8 ignore next 3 */
} else {
console.warn(err)
}
}
lines.forEach((line, idx) => {
line = line.trim()
if (line === '') {
if (params.onOrphanComment) {
params.onOrphanComment(currentComment)
}
currentComment = ''
return
}
if (line.match(/^\/\//)) {
if (currentComment) {
if (line[2] === '-') {
currentComment += `\n${line.substring(3).trim()}`
} else {
currentComment += ` ${line.substring(2).trim()}`
}
} else {
currentComment = line.substring(2).trim()
}
return
}
if (line === '---functions---') {
currentKind = 'method'
return
}
if (line === '---types---') {
currentKind = 'class'
return
}
const match = SINGLE_REGEX.exec(line)
if (!match) {
2023-11-09 21:03:52 +03:00
handleError(new Error(`Failed to parse line ${idx + 1}: ${line}`), idx + 1)
return
}
const [, typeName, typeId, generics, args, type] = match
if (typeName in TL_PRIMITIVES) {
return
}
let typeIdNum = typeId ? Number.parseInt(typeId, 16) : 0
if (typeIdNum === 0 && !params.forIdComputation) {
typeIdNum = computeConstructorIdFromString(line)
}
const argsParsed
= args && !args.match(/\[ [a-z]+ \]/i)
? args
.trim()
.split(' ')
.map(j => j.split(':'))
: []
const entry: TlEntry = {
kind: currentKind,
name: typeName,
id: typeIdNum,
type,
arguments: [],
}
if (entry.kind === 'method' && params.parseMethodTypes) {
const [type, modifiers] = parseArgumentType(entry.type)
entry.type = type
if (Object.keys(modifiers).length) {
entry.typeModifiers = modifiers
}
}
if (generics) {
entry.generics = generics.split(',').map((it) => {
const [name, type] = it.split(':')
return { name, type }
})
}
if (argsParsed.length) {
argsParsed.forEach(([name, type_]) => {
const [type, modifiers] = parseArgumentType(type_)
const item: TlArgument = {
name,
type,
}
if (Object.keys(modifiers).length) {
item.typeModifiers = modifiers
}
entry.arguments.push(item)
})
}
if (currentComment) {
if (currentComment.match(/^@description /)) {
// tdlib-style comment
const obj = parseTdlibStyleComment(currentComment)
if (obj.description) entry.comment = obj.description
entry.arguments.forEach((arg) => {
if (arg.name in obj) {
arg.comment = obj[arg.name]
}
})
} else {
entry.comment = currentComment
}
currentComment = ''
}
ret.push(entry)
entries[entry.name] = entry
if (entry.kind === 'class') {
if (!unions[entry.type]) unions[entry.type] = []
unions[entry.type].push(entry)
}
})
if (currentComment && params.onOrphanComment) {
params.onOrphanComment(currentComment)
}
2023-11-09 21:03:52 +03:00
if (params.forIdComputation) return ret
// post-process:
// - add return type ctor id for methods
// - find arguments where type is not a union and put corresponding modifiers
// - apply prefix
ret.forEach((entry, entryIdx) => {
if (params.parseMethodTypes && entry.kind === 'method') {
const type = entry.type
if (type in unions && unions[type].length === 1) {
if (!entry.typeModifiers) entry.typeModifiers = {}
entry.typeModifiers.constructorId = unions[type][0].id
} else if (type in entries) {
if (!entry.typeModifiers) entry.typeModifiers = {}
entry.typeModifiers.isBareType = true
entry.typeModifiers.constructorId = entries[type].id
}
}
entry.arguments.forEach((arg) => {
const type = arg.type
if (type in TL_PRIMITIVES) {
return
}
2023-11-09 21:03:52 +03:00
if (arg.typeModifiers?.isBareUnion) {
if (!(type in unions)) {
handleError(new Error(`Union ${type} not found (found in ${entry.name}#${arg.name})`), entryIdx)
} else if (unions[type].length !== 1) {
handleError(
new Error(
`Union ${type} has more than one entry, cannot use it like %${type} (found in ${entry.name}#${arg.name})`,
),
entryIdx,
)
2023-11-09 21:03:52 +03:00
} else {
arg.typeModifiers.constructorId = unions[type][0].id
}
} else if (type in entries) {
if (!arg.typeModifiers) arg.typeModifiers = {}
arg.typeModifiers.isBareType = true
arg.typeModifiers.constructorId = entries[type].id
if (prefix) {
arg.type = prefix + arg.type
}
}
})
if (prefix) {
entry.name = prefix + entry.name
}
})
return ret
}