export interface FunctionDef { name: string, signatures: Array<{ args: Array<{ name: string, type: string }> // FIXME: remove undefined in the end returns: string | undefined }> } export interface DependencyDef extends FunctionDef { aliasOf: string | undefined } /** * Parse a reflected type coming out of TypeScript into a structured object, for example: * * '(dep: configDependency & { complex: ((re: number) => Complex) | ((re: number, im: number) => Complex); }) => (a: number) => number | Complex' */ export function parseReflectedType(name: string, reflectedType: string) : { fn: FunctionDef, dependencies: Record } { const [factoryArgs, fnArgsBlock, returns] = split(reflectedType, '=>').map(trim) const args = parseArgs(fnArgsBlock) const factoryArgsInner = findBlockContents(factoryArgs, '(', ')') const depArg = split(factoryArgsInner.innerText, ':').map(trim)[1] const depArgBlocks = split(depArg, '&').map(trim) const deps = depArgBlocks .filter(depArgBlock => { if (depArgBlock.startsWith('{')) { return true } else { console.error(`ERROR: Cannot parse dependency "${depArgBlock}"`) } }) .flatMap(parseDependencies) const dependencies: Record = groupBy(deps, 'name') return { fn: { name, signatures: [{args, returns}] }, dependencies } function parseDependencies(deps: string) { const inner = findBlockContents(deps, '{', '}').innerText return split(inner, ';') .map(trim) .filter(notEmpty) .map(parseDependency) } // parse a dependency like "complex: ((re: number) => Complex) | ((re: number, im: number) => Complex)" function parseDependency(dep: string) { const [name, def] = split(dep, ':').map(trim) const { aliasOf, innerSignature } = parseAliasOf(def) const signatures = split(innerSignature, '|') .map(trim) .map(stripParenthesis) .map(signature => { const [argsBlock, returns] = split(signature, '=>').map(trim) if (!returns) { console.warn(`ERROR: failed to find return type in '${signature}'`) } return { args: parseArgs(argsBlock), returns } }) return { name, signatures, aliasOf } } // parse args like "(re: number, im: number)" function parseArgs(argsBlock: string) : Array<{name: string, type: string}> { const args = findBlockContents(argsBlock, '(', ')').innerText return split(args, ',') .map(trim) .map(arg => { const [name, type] = split(arg, ':').map(trim) return { name, type} }) } // parse "AliasOf<"divide", (a: Complex, b: RealType>) => Complex>" function parseAliasOf(signature: string) : { innerSignature: string, aliasOf: string | undefined } { if (!signature.startsWith('AliasOf')) { return { innerSignature: signature, aliasOf: undefined } } const inner = findBlockContents(signature, '<', '>').innerText.trim() const [aliasOfWithQuotes, innerSignature] = split(inner, ',').map(trim) return { innerSignature, aliasOf: aliasOfWithQuotes.substring(1, aliasOfWithQuotes.length - 1) // remove double quotes } } // remove the outer parenthesis, for example "((re: number) => Complex)" returns "(re: number) => Complex" function stripParenthesis(text: string) : string { return text.startsWith('(') && text.endsWith(')') ? text.substring(1, text.length - 1) : text } } function findBlockContents(text: string, blockStart: string, blockEnd: string, startIndex = 0) : { start: number, end: number, innerText: string } | undefined { let i = startIndex while (!matchSubString(text, blockStart, i) && i < text.length) { i++ } if (i >= text.length) { return undefined } i++ const start = i while (!matchSubString(text, blockEnd, i) || matchSubString(text, '=>', i - 1)) { i = skipBrackets(text, i) i++ } if (i >= text.length) { return undefined } const end = i return { start, end, innerText: text.substring(start, end) } } /** * Split a string by a delimiter, but ignore all occurrences of the delimiter * that are inside bracket pairs <> () [] {} */ export function split(text: string, delimiter: string) : string[] { const parts: string[] = [] let i = 0 let start = 0 while (i < text.length) { i = skipBrackets(text, i) if (matchSubString(text, delimiter, i)) { parts.push(text.substring(start, i)) i += delimiter.length start = i } i++ } parts.push(text.substring(start)) return parts } function skipBrackets(text: string, startIndex: number) : number { let level = 0 let i = startIndex do { if (isBracketOpen(text, i)) { level++ } if (isBracketClose(text, i) && level > 0) { level-- } if (level === 0) { break } i++ } while(i < text.length) return i } function isBracketOpen(text: string, index: number) { const char = text[index] return char === '(' || char === '<' || char === '[' || char === '{' } function isBracketClose(text: string, index: number) { const char = text[index] // we need to take care of not matching the ">" of the operator "=>" return char === ')' || (char === '>' && text[index - 1] !== '=') || char === ']' || char === '}' } function matchSubString(text: string, search: string, index: number) : boolean { for (let i = 0; i < search.length; i++) { if (text[i + index] !== search[i]) { return false } } return true } function trim(text: string) : string { return text.trim() } function notEmpty(text: string) : boolean { return text.length > 0 } function groupBy(items: T[], key: string) : Record { const obj: Record = {} items.forEach((item) => { obj[item[key]] = item }) return obj }