UNPKG

@reyalp/debug-utils

Version:

ts transformers for debug

689 lines (542 loc) 30.3 kB
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) }