export type FunctionDef = { name: string, aliasOf?: string, signatures: Array<{ args: Array<{ name: string, type: string }> returns: string }> } export type ImplementationDef = { fn: FunctionDef, dependencies: Record } /** * 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): ImplementationDef { console.log('For', name, 'parsing', reflectedType) const [factoryArgs, fnsClause] = split(reflectedType, '=>', 2).map(trim) const fn = parseAlias(name, fnsClause) const factoryArgsInner = findBlockContents(factoryArgs, '(', ')') const depArg = split(factoryArgsInner.innerText, ':').map(trim)[1] const depArgBlocks: string[] = depArg ? split(depArg, '&').map(trim) : [] const deps = depArgBlocks .filter(depArgBlock => { if (depArgBlock.startsWith('{') || depArgBlock === 'configDependency') { return true } else { throw new SyntaxError(`Cannot parse dependency "${depArgBlock}"`) } }) .flatMap(parseDependencies) const dependencies: Record = groupBy(deps, 'name') return {fn, dependencies} } function parseDependencies(deps: string): FunctionDef[] { if (deps === 'configDependency') { return [{name: 'config', signatures: [{args: [], returns: 'Config'}]}] } 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): FunctionDef { const [name, def] = split(dep, ':').map(trim) return parseAlias(name, def) } // Parse a possibly aliased function function parseAlias(name: string, alias: string): FunctionDef { const { aliasOf, innerSignature } = parseAliasOf(alias) return { name, signatures: parseSignatures(innerSignature), aliasOf } } // parse function signatures like ((re: number) => Complex) | ((re: number, im: number) => Complex) // But also have to succeed on (a: number) => number | Complex // That's why we only split on an alternation bar `|` that's followed by // a parenthesis; that way we avoid splitting a union return type. Note // this is not necessarily foolproof, as there could be a return type that // is a union with a complicated piece that has to be enclosed in parens; // but so far it seems to work in practice. function parseSignatures(sigs: string) { return split(sigs, /[|]\s*(?=[(])/) .map(trim) .map(stripParenthesis) .map(signature => { const [argsBlock, returns] = split(signature, '=>').map(trim) if (!returns) { throw new SyntaxError(`Failed to find return type in '${signature}'`) } return { args: parseArgs(argsBlock), returns } }) } // 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) } } /** * Given a string, generate a string source for a regexp that will match * exactly the given string. * Uses the fact that the only characters that need to be escaped in * a character class are \, ], and ^ */ function regexpQuote(s: string) { const special = '\\]^' let re = '' for (const char of s) { if (special.includes(char)) re += `\\${char}` else re += `[${char}]` } return re } /** * 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 | RegExp, pieces = 0): string[] { const delim: RegExp = typeof delimiter === 'string' ? new RegExp(regexpQuote(delimiter), 'y') : new RegExp(delimiter.source, 'y') const parts: string[] = [] let i = 0 let n = 1 let start = 0 while (i < text.length && (pieces === 0 || n < pieces)) { i = skipBrackets(text, i) delim.lastIndex = i const result = delim.exec(text) if (result) { parts.push(text.substring(start, i)) n += 1 i += result[0].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 }