@reyalp/debug-utils
Version:
ts transformers for debug
689 lines (542 loc) • 30.3 kB
text/typescript
import path, { join, parse } from 'path'
import resolve from 'resolve'
import ts from 'typescript'
import { LogLevel } from './logger'
import type { SourceLocation, SourceLocationSymbol } from './source-location'
type Compiler = { addDiagnostic(diag: ts.Diagnostic): number }
const package_name = '@reyalp/debug-utils'
const modules = {
logger: {
types: _enum('Logger', 'LogCallChain1', 'LogCallChain2'),
rt: _enum('format', 'filter', 'inspect', 'write'),
chained: _enum('msg', 'dump', 'trace'),
methods: _enum('tag', 'error', 'warn', 'info', 'verbose', 'debug', 'silly'),
levels: {
error: LogLevel.ERROR,
warn: LogLevel.WARN,
info: LogLevel.INFO,
verbose: LogLevel.VERBOSE,
debug: LogLevel.DEBUG,
silly: LogLevel.SILLY
} as Record<string, LogLevel>
},
introspect: _enum('introspect', 'quoteval')
}
export const transform = (program: ts.Program, opts: { progress?: boolean } | undefined, compiler: Compiler) => (context: ts.TransformationContext) => {
const options = program.getCompilerOptions()
const common_dir = program.getCommonSourceDirectory()
const root_dirs = [...(options.rootDirs ?? []), options.rootDir ?? '', common_dir.endsWith('/') ? common_dir : (common_dir + '/')].filter(Boolean)
const printer = ts.createPrinter({ omitTrailingSemicolon: true, removeComments: true, preserveSourceNewlines: false })
const checker = program.getTypeChecker()
const factory = context.factory
const host = ts.createCompilerHost(program.getCompilerOptions())
return (source: ts.SourceFile) => {
const source_dir = parse(source.fileName).dir
const resolved = ts.resolveModuleName(package_name, source.fileName, program.getCompilerOptions(), host)
const resolved_lib_index = resolved.resolvedModule?.resolvedFileName ?? resolve.sync(package_name, { basedir: source_dir })
const resolved_lib_dir = parse(resolved_lib_index).dir
if (opts?.progress) {
console.log(`TSC: ${source.fileName.slice(root_dirs.find(r => source.fileName.startsWith(r))?.length ?? 0)}`)
}
void _debug_print_node
return ts.visitNode(source, visit)
// === SOURCE_LOCATION =============================================================================
function format_node_name(node: ts.Node, fallback = '<expr>'): string {
return ts.isPropertyAccessExpression(node) ? format_call_expr(node, fallback)
: ts.isCallExpression(node) ? format_prop_access_expr(node, fallback)
: is_simple_node(node) ? printer.printNode(ts.EmitHint.Expression, node, source)
: (fallback + _debug_print_node(node))
}
function format_prop_access_expr(node: ts.CallExpression, fallback = '<expr>'): string {
const fn = _strip_paren_down(node.expression)
return ts.isPropertyAccessExpression(fn) ? format_call_expr(fn, fallback) : format_node_name(fn, fallback)
}
function format_call_expr(node: ts.PropertyAccessExpression, fallback = '<expr>'): string {
if (ts.isCallExpression(node.expression)) {
const lhs = format_prop_access_expr(node.expression, fallback) ?? format_node_name(node, fallback)
return lhs + '.' + format_node_name(node.name, fallback)
} else {
return format_node_name(node.expression, fallback) + '.' + format_node_name(node.name, fallback)
}
}
function collect_source_location(node: ts.Node, ups: number) {
const loc = ts.getLineAndCharacterOfPosition(source, node.getStart(source))
const line = loc.line + 1
const column = loc.character + 1
const file = collect_file_info(source.fileName, root_dirs)
function _mk_item(consume: (node: ts.Node) => Pick<SourceLocationSymbol, 'kind' | 'name'>) {
return (node: ts.Node) => {
const { kind, name } = consume(node)
const loc = ts.getLineAndCharacterOfPosition(source, node.getStart(source))
const line = loc.line + 1
const column = loc.character + 1
return { kind, name, line, column }
}
}
const spine = unwind(node, ups)
const symbols: SourceLocation['symbols'] = spine.map(_mk_item(root => {
if (!root) return { kind: 'toplevel', name: '<toplevel>' }
if (ts.isSourceFile(root)) return { kind: 'toplevel', name: '<toplevel>' }
if (ts.isArrowFunction(root)) return { kind: 'func', name: 'λ' }
if (ts.isConstructorDeclaration(root)) return { kind: 'func', name: '(ctor)' }
if (ts.isFunctionDeclaration(root)) return { kind: 'func', name: root.name ? format_node_name(root.name) : 'λ' }
if (ts.isFunctionExpression(root)) return { kind: 'func', name: root.name ? format_node_name(root.name) : 'λ' }
if (ts.isCallExpression(root)) return { kind: 'call', name: format_node_name(root.expression, '<call>') }
if (ts.isArrayLiteralExpression(root)) return { kind: 'elem', name: '[]' }
if (ts.isPropertyAssignment(root)) return { kind: 'prop', name: format_node_name(root.name, '<assign>') }
if (ts.isPropertyDeclaration(root)) return { kind: 'prop', name: format_node_name(root.name, '<assign>') }
if (ts.isClassDeclaration(root)) return { kind: 'class', name: root.name?.text ?? '*Annoymous' }
if (ts.isMethodDeclaration(root)) return { kind: 'func', name: format_node_name(root.name) }
if (ts.isModuleDeclaration(root)) return { kind: 'namespace', name: format_node_name(root.name) }
if (ts.isVariableDeclaration(root)) {
if (ts.isArrayBindingPattern(root.name)) return { kind: 'var', name: '[]' }
if (ts.isObjectBindingPattern(root.name)) return { kind: 'var', name: '{}' }
if (ts.isEmptyBindingPattern(root.name)) return { kind: 'var', name: '{}' }
return { kind: 'var', name: format_node_name(root.name) }
}
if (ts.isAssignmentExpression(root)) {
if (ts.isArrayLiteralExpression(root.left)) return { kind: 'var', name: '[]' }
return { kind: 'var', name: format_node_name(root.left, '<assign>') }
}
return { kind: 'expr', name: format_node_name(root, `<${ts.SyntaxKind[root.kind]}>`) }
}))
const sympath = symbols.filter(c => c.kind !== 'toplevel').map(c => c.name).reverse().join('.') || '<toplevel>'
return { line, column, symbols, spine, file, sympath }
}
function _flatten_source_location_expr_prop_chain(node: ts.Expression) {
const path: string[] = []
while (ts.isPropertyAccessExpression(node)) {
path.push(node.name.text)
node = _strip_paren_down(node.expression)
}
// node should be ts.Identifier
if (ts.isIdentifier(node)) path.push(node.text)
return path.reverse()
}
function _is_source_location_expr(node: ts.Expression) {
if (_should_skip(node)) return false
const root = _get_prop_chain_root(node)
if (!root) return false
const ty = checker.getTypeAtLocation(root)
const tyname = checker.typeToString(ty)
if (tyname !== 'SourceLocation') return false
const sym = checker.getSymbolAtLocation(root) // should be `source_location`
if (!sym) return false
const decls = sym.getDeclarations()
if (!decls || decls.length === 0) return false
return _is_imported_from_this_package(decls[0])
}
// path is something like `source_location` / `source_location.file.name` / `source_location.up`...
function _create_source_location_expr(node: ts.Node, path: string[]) {
const ups = path.slice(1).filter(t => t === 'up')
const trail = path.slice(ups.length + 1).join('.')
const info = collect_source_location(node, ups.length)
const full = `${info.file.rel_path}:${info.line} ${info.sympath}`
const create_symbols = () => factory.createArrayLiteralExpression(info.symbols.map(s => factory.createObjectLiteralExpression([
factory.createPropertyAssignment('kind', factory.createStringLiteral(s.kind)),
factory.createPropertyAssignment('name', factory.createStringLiteral(s.name)),
factory.createPropertyAssignment('line', factory.createNumericLiteral(s.line)),
factory.createPropertyAssignment('column', factory.createNumericLiteral(s.column))
])))
const create_file = () => factory.createObjectLiteralExpression(
Object.entries(info.file).map(([name, val]) => factory.createPropertyAssignment(name, factory.createStringLiteral(val)))
)
if (trail === 'symbols') return create_symbols()
if (trail === 'file') return create_file()
if (trail === 'sympath') return factory.createStringLiteral(info.sympath)
if (trail === 'full') return factory.createStringLiteral(full)
const { line, column } = info
for (const [name, val] of Object.entries({ line, column })) {
if (trail === name) return factory.createNumericLiteral(val)
}
for (const [name, val] of Object.entries(info.file)) {
if (trail === 'file.' + name) return factory.createStringLiteral(val)
}
return factory.createObjectLiteralExpression([
factory.createPropertyAssignment('line', factory.createNumericLiteral(line)),
factory.createPropertyAssignment('column', factory.createNumericLiteral(column)),
factory.createPropertyAssignment('sympath', factory.createStringLiteral(info.sympath)),
factory.createPropertyAssignment('full', factory.createStringLiteral(full)),
factory.createPropertyAssignment('symbols', create_symbols()),
factory.createPropertyAssignment('file', create_file())
])
}
function _check_source_location_param_initializer(node: ts.CallExpression | ts.NewExpression) {
const sig = checker.getResolvedSignature(node)
if (!sig) return undefined
const decl = sig.getDeclaration()
if (!decl) return undefined
const parameters = decl.parameters
const index = parameters.findIndex(p => p.initializer && _is_source_location_expr(p.initializer))
if (index === -1) return undefined
return { index, initializer: parameters[index].initializer! }
}
function _try_transform_source_location_call_param(node: ts.Node): ts.Node | undefined {
if (!ts.isCallExpression(node)) return undefined
const p = _check_source_location_param_initializer(node)
if (!p) return undefined
const fn = visit<ts.Expression>(node.expression)
const initializer = _strip_paren_down(p.initializer)
const path = _flatten_source_location_expr_prop_chain(initializer)
const sl = _create_source_location_expr(node, path)
const args = _list(Math.max(node.arguments.length, p.index + 1))
.map(i => node.arguments[i] ? visit<ts.Expression>(node.arguments[i]) : factory.createVoidZero())
.map((expr, i) => i === p.index ? factory.createBinaryExpression(expr, ts.SyntaxKind.QuestionQuestionToken, sl) : expr)
return factory.updateCallExpression(node, fn, node.typeArguments, args)
}
function _try_transform_source_location_expr(node: ts.Node) {
const parent = _strip_paren_up(node.parent)
if (parent && ts.isParameter(parent)) return undefined
if (!ts.isExpression(node)) return undefined
if (!_is_source_location_expr(node)) return undefined
const path = _flatten_source_location_expr_prop_chain(node) // path[0] is always source_location (or it's alias)
return _create_source_location_expr(node, path)
}
// =================================================================================================
// === INTROSPECTION ===============================================================================
function _check_introspection_call(node: ts.CallExpression) {
const decl = checker.getResolvedSignature(node)?.declaration
if (!decl) return undefined
if (!ts.isFunctionDeclaration(decl)) return undefined
if (!decl.name) return undefined
if (!ts.isIdentifier(decl.name)) return undefined
if (!modules.introspect[decl.name.text]) return undefined
const fn = node.expression
if (!ts.isIdentifier(fn)) return undefined
const sym = checker.getSymbolAtLocation(fn)
if (!sym) return undefined
const decls = sym.getDeclarations()
if (!decls || decls.length === 0) return undefined
if (!_is_imported_from_this_package(decls[0])) return undefined
return decl.name.text
}
function _try_transform_introspection_call(node: ts.Node) {
if (!ts.isCallExpression(node)) return undefined
const fn = _check_introspection_call(node)
if (!fn) return undefined
if (fn === modules.introspect.introspect) {
const code = printer.printNode(ts.EmitHint.Expression, node.arguments[0], source)
const arg = visit(node.arguments[0]) as ts.Expression
return factory.createArrayLiteralExpression([factory.createStringLiteral(code), arg])
} else if (fn === modules.introspect.quoteval) {
// quoteval: (expr, j, print)
const code = printer.printNode(ts.EmitHint.Expression, node.arguments[0], source)
const args = node.arguments.map(visit) as ts.Expression[]
const j_fallback = factory.createStringLiteral(' = ')
const j = args[1] ? factory.createBinaryExpression(args[1], ts.SyntaxKind.QuestionQuestionToken, j_fallback) : j_fallback
const p_fallback = factory.createPropertyAccessExpression(node.expression, '_inspect')
const p_fn = args[2] ? factory.createBinaryExpression(args[2], ts.SyntaxKind.QuestionQuestionToken, p_fallback) : p_fallback
const p = factory.createCallExpression(p_fn, undefined, [args[0]])
return factory.createTemplateExpression(factory.createTemplateHead(code), [
factory.createTemplateSpan(j, factory.createTemplateMiddle('')),
factory.createTemplateSpan(p, factory.createTemplateTail(''))
])
}
}
function _validate_introspection_usage(node: ts.Node) {
if (_should_skip(node)) return
if (!ts.isIdentifier(node)) return
if (ts.isImportSpecifier(node.parent)) return
const sym = checker.getSymbolAtLocation(node)
if (!sym) return
const decls = sym.getDeclarations()
if (!decls || decls.length === 0) return
const decl = decls[0]
if (!ts.isImportSpecifier(decl)) return
if (!_is_imported_from_this_package(decl)) return
const name = _import_names(decl)
if (!modules.introspect[name]) return
if (ts.isCallExpression(node)) return
compiler.addDiagnostic(_make_diag(node, 'invalid introspect usage', `only '${node.text}(...)' is allowed`))
}
// =================================================================================================
// === LOGGER ======================================================================================
function _check_log_call(node: ts.CallExpression) {
const fn = _strip_paren_down(node.expression)
if (!ts.isPropertyAccessExpression(fn)) return undefined
const lhs = fn.expression
const ty = checker.getTypeAtLocation(lhs)
const tyname = checker.typeToString(ty)
if (!modules.logger.types[tyname]) return undefined
if (!modules.logger.chained[fn.name.text] && !modules.logger.methods[fn.name.text]) return undefined
const decl = ty.symbol?.getDeclarations()?.[0]
if (!decl) return undefined
let decl_source = decl.parent
while (decl_source && !ts.isSourceFile(decl_source)) {
decl_source = decl_source.parent
}
return decl_source.fileName === join(resolved_lib_dir, 'logger.d.ts') ? tyname : undefined
}
function _flatten_log_call_chain(node: ts.CallExpression) {
/*
* CallExpression
* / \
* PropertyAccessExpression (args)
* / \
* CallExpression Identifier (method name)
* ........
*/
const chain: { name: string; args: ts.Expression[] }[] = []
let start = node as ts.Expression
while (ts.isCallExpression(start)) {
const ty = _check_log_call(start)
if (!ty) break
const args = [...start.arguments]
const prop = _strip_paren_down(start.expression)
if (!ts.isPropertyAccessExpression(prop)) {
// not possible...
throw new Error(`call.parent is not PropertyAccessExpression!`)
}
chain.push({ name: prop.name.text, args })
start = prop.expression
}
return { start, chain: chain.reverse() }
}
function _log_format_arg(expr: ts.Expression, rt: ts.Expression) {
if (ts.isLiteralExpression(expr)) return expr
const node = visit<ts.Expression>(expr)
const ty = checker.getTypeAtLocation(expr)
if (ty.getFlags() & (ts.TypeFlags.StringLike)) {
return node
}
if (ty.getFlags() & (ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.EnumLike)) {
return node
}
return factory.createCallExpression(
factory.createPropertyAccessExpression(rt, modules.logger.rt.inspect),
undefined,
[node, factory.createStringLiteral(checker.typeToString(ty)), rt]
)
}
function _log_format_dump_arg(expr: ts.Expression, rt: ts.Expression) {
return factory.createTemplateExpression(
factory.createTemplateHead(`${printer.printNode(ts.EmitHint.Expression, expr, source)} = `),
[factory.createTemplateSpan(_log_format_arg(expr, rt), factory.createTemplateTail(''))]
)
}
type CallChain = ReturnType<typeof _flatten_log_call_chain>
function _create_log_stmts(call: ts.CallExpression, { chain, start }: CallChain = _flatten_log_call_chain(call)) {
// <logger>.<method₁>(args₁).<method₂>(args₂)...
// =>
// {
// var logger = <logger>, rt = logger.rt, level = <level>, tags = <tags>
// if (rt.filter(ll, tags, rt)) {
// var file = <file>, line = <line>, name = <name>
// rt.write(rt.format([...], ll, tags, file, line, name, rt), ll, tags, file, line, name, rt)
// }
// }
const logger_var = ts.isIdentifier(start) ? start : factory.createUniqueName('logger')
const rt_var = factory.createUniqueName('rt')
const tags_var = factory.createUniqueName('tags')
const level_var = factory.createUniqueName('log_level')
const extra_tags = chain.filter(c => c.name === modules.logger.methods.tag).flatMap(c => c.args).map(visit<ts.Expression>)
const log_level_call = chain.find(c => modules.logger.levels[c.name])
const log_level = log_level_call ? modules.logger.levels[log_level_call.name] : LogLevel.INFO
const args = chain
.filter(c => c.name !== modules.logger.methods.tag && c.name !== modules.logger.chained.trace)
.flatMap(c => c.args.map(arg => c.name === modules.logger.chained.dump ? _log_format_dump_arg(arg, rt_var) : _log_format_arg(arg, rt_var)))
const logger_tags = factory.createPropertyAccessExpression(logger_var, 'tags')
const tags_init = extra_tags.length === 0 ? logger_tags : factory.createArrayLiteralExpression([
factory.createSpreadElement(logger_tags), ...extra_tags
])
const decl_vars = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
...ts.isIdentifier(start) ? [] : [factory.createVariableDeclaration(logger_var, undefined, undefined, start)],
factory.createVariableDeclaration(rt_var, undefined, undefined, factory.createPropertyAccessExpression(logger_var, 'rt')),
factory.createVariableDeclaration(tags_var, undefined, undefined, tags_init),
factory.createVariableDeclaration(level_var, undefined, undefined, factory.createNumericLiteral(log_level))
]))
const pred_expr = factory.createCallExpression(
factory.createPropertyAccessExpression(rt_var, modules.logger.rt.filter),
undefined,
[level_var, tags_var, rt_var]
)
const info = collect_source_location(call, 1)
const file_var = factory.createUniqueName('file')
const line_var = factory.createUniqueName('line')
const name_var = factory.createUniqueName('name')
const trace_cfg = chain.find(c => c.name === modules.logger.chained.trace)?.args?.[0]
const trace_cfg_expr = trace_cfg && visit<ts.Expression>(trace_cfg)
const wrap_trace_init = (expr: ts.Expression) =>
!trace_cfg_expr ? expr
: !ts.isBooleanLiteral(trace_cfg_expr) ? factory.createConditionalExpression(trace_cfg_expr, undefined, expr, undefined, factory.createVoidZero())
: trace_cfg_expr.kind === ts.SyntaxKind.TrueKeyword ? expr : factory.createVoidZero()
const write_decls = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
factory.createVariableDeclaration(file_var, undefined, undefined, wrap_trace_init(factory.createStringLiteral(join(info.file.rel_dir, info.file.name)))),
factory.createVariableDeclaration(line_var, undefined, undefined, wrap_trace_init(factory.createNumericLiteral(info.line))),
factory.createVariableDeclaration(name_var, undefined, undefined, wrap_trace_init(factory.createStringLiteral(info.sympath)))
]))
const trace_args = [level_var, tags_var, rt_var, file_var, line_var, name_var]
const format_call = factory.createCallExpression(
factory.createPropertyAccessExpression(rt_var, modules.logger.rt.format),
undefined,
[factory.createArrayLiteralExpression(args), ...trace_args]
)
const write_call = factory.createCallExpression(
factory.createPropertyAccessExpression(rt_var, modules.logger.rt.write),
undefined,
[format_call, ...trace_args]
)
const write_stmt = factory.createBlock([write_decls, factory.createExpressionStatement(write_call)])
const log_stmt = factory.createIfStatement(pred_expr, write_stmt)
return [decl_vars, log_stmt]
}
function _try_transform_log_call(node: ts.Node) {
if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && _check_log_call(node.expression)) {
return factory.createBlock(_create_log_stmts(node.expression))
} else if (ts.isCallExpression(node) && _check_log_call(node)) {
const call_chain = _flatten_log_call_chain(node)
const logger_var = factory.createUniqueName('logger')
const decl = factory.createVariableDeclaration(logger_var, undefined, undefined, call_chain.start)
return factory.createImmediatelyInvokedArrowFunction([
factory.createVariableStatement(undefined, [decl]),
..._create_log_stmts(node, { chain: call_chain.chain, start: logger_var }),
factory.createReturnStatement(logger_var)
])
}
return undefined
}
// =================================================================================================
function _debug_print_node(node: ts.Node) {
return `${printer.printNode(ts.EmitHint.Unspecified, node!, source)} :: ${ts.SyntaxKind[node.kind]}`
}
function _validate_import(node: ts.Node) {
if (!ts.isImportClause(node)) return
const nb = node.namedBindings
if (!node.name && (!nb || !ts.isNamespaceImport(nb))) return
const specifier = node.parent?.moduleSpecifier
if (!ts.isStringLiteral(specifier)) return
if (specifier.text !== package_name) return
compiler.addDiagnostic(_make_diag(node.parent, `module specifier`, `use import { ... } from '${package_name}' instead`))
}
function validate_node(node: ts.Node) {
_validate_import(node)
_validate_introspection_usage(node)
}
function next(node: ts.Node) {
return ts.visitEachChild(node, visit, context)
}
function visit<N extends ts.Node = ts.Node>(node: ts.Node): N {
validate_node(node)
return (_try_transform_log_call(node)
?? _try_transform_introspection_call(node)
?? _try_transform_source_location_call_param(node)
?? _try_transform_source_location_expr(node)
?? next(node)
) as N
}
}
}
export default transform
const is_stop_root = (node: ts.Node) => ts.isArrowFunction(node)
|| ts.isFunctionExpression(node)
|| ts.isFunctionDeclaration(node)
|| ts.isModuleDeclaration(node)
|| ts.isClassDeclaration(node)
|| ts.isClassExpression(node)
|| ts.isVariableDeclaration(node)
|| ts.isMethodDeclaration(node)
|| ts.isMethodDeclaration(node)
|| ts.isPropertyDeclaration(node)
|| ts.isPropertyAssignment(node)
|| ts.isAssignmentExpression(node)
|| ts.isConstructorDeclaration(node)
|| ts.isParameter(node)
|| ts.isCallExpression(node)
function seek_stop_root(node: ts.Node) {
while (node && node.parent && !is_stop_root(node)) {
node = _strip_paren_up(node.parent)
}
return node
}
function skip_stop_root(node: ts.Node) {
while (node) {
if (!ts.isCallExpression(node)) return _strip_paren_up(node.parent)
// node is CallExpression
node = _strip_paren_up(node.parent)
while (ts.isPropertyAccessExpression(node)) {
node = _strip_paren_up(node.parent)
}
}
return node
}
function unwind(node: ts.Node, ups: number) {
const spine: ts.Node[] = []
while (node && node.parent) {
spine.push(node = seek_stop_root(node))
node = skip_stop_root(node)
}
return spine.slice(ups)
}
function is_simple_node(node: ts.Node): boolean {
if (node.kind === ts.SyntaxKind.ThisKeyword) return true
if (ts.isIdentifier(node)) return true
if (ts.isPrivateIdentifier(node)) return true
if (ts.isNoSubstitutionTemplateLiteral(node)) return true
if (ts.isStringLiteral(node)) return true
if (ts.isNumericLiteral(node)) return true
if (ts.isBigIntLiteral(node)) return true
if (ts.isPropertyAccessExpression(node)) return is_simple_node(node.expression) && is_simple_node(node.name)
if (ts.isElementAccessExpression(node)) return is_simple_node(node.expression) && is_simple_node(node.argumentExpression)
if (ts.isCallExpression(node)) return is_simple_node(node.expression) && node.arguments.every(is_simple_node)
if (ts.isComputedPropertyName(node)) return is_simple_node(node.expression)
return false
}
function trim_leading_slash(s: string) {
return (s !== '/' && s.startsWith('/')) ? s.slice(1) : s
}
function collect_file_info(full_path: string, root_dirs: string[]) {
const p = path.parse(full_path)
const rel = root_dirs.find(dir => full_path.startsWith(dir))
const full_dir = p.dir
const rel_path = trim_leading_slash(full_path.slice(rel?.length ?? 0))
const rel_dir = trim_leading_slash(full_dir.slice(rel?.length ?? 0))
const ext = p.ext
const name = p.name
const fullname = p.base
return { full_path, full_dir, rel_path, rel_dir, ext, name, fullname }
}
function _get_prop_chain_root(node: ts.Expression) {
while (ts.isPropertyAccessExpression(node)) {
node = _strip_paren_down(node.expression)
}
return ts.isIdentifier(node) ? node : undefined
}
function _is_imported_from_this_package(decl: ts.Declaration) {
if (!ts.isImportSpecifier(decl)) return false
const import_decl = decl.parent.parent.parent
const specifier = import_decl.moduleSpecifier
if (!ts.isStringLiteral(specifier)) return false
return specifier.text === package_name
}
function _enum<K extends string>(...names: K[]) {
return Object.fromEntries(names.map(k => [k, k])) as { [P in K]: P } & Record<string, string>
}
function _import_names(decl: ts.ImportSpecifier) {
return (decl.propertyName ?? decl.name).text
}
function _make_diag(node: ts.Node, key: string, message: string, category = ts.DiagnosticCategory.Error) {
const code = `(${package_name})` as unknown as number
return ts.createDiagnosticForNode(node, { code, key, category, message })
}
function _list(length: number) {
return Array.from({ length }).map((_, i) => i)
}
function _strip_paren_up(node: ts.Node) {
while (node && ts.isParenthesizedExpression(node)) {
node = node.parent
}
return node
}
function _strip_paren_down(node: ts.Expression) {
while (ts.isParenthesizedExpression(node)) {
node = node.expression
}
return node
}
function _should_skip(node: ts.Node) {
return !node.parent
|| ts.isImportSpecifier(node.parent)
|| ts.isJsxOpeningElement(node.parent)
|| ts.isJsxClosingElement(node.parent)
|| ts.isJsxSelfClosingElement(node.parent)
}