@pikku/inspector 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.ts ADDED
@@ -0,0 +1,863 @@
1
+ import * as ts from 'typescript'
2
+ import { InspectorFilters } from './types.js'
3
+
4
+ type ExtractedFunctionName = {
5
+ pikkuFuncName: string
6
+ name: string
7
+ exportedName: string | null
8
+ functionName: string | null
9
+ propertyName: string | null
10
+ }
11
+
12
+ /**
13
+ * Generate a deterministic "anonymous" name for any expression node,
14
+ * but if it's an Identifier pointing to a function, resolve it back
15
+ * to the function's declaration (so you get the true source location).
16
+ */
17
+ export function makeDeterministicAnonName(
18
+ start: ts.Node,
19
+ checker: ts.TypeChecker
20
+ ): string {
21
+ let node: ts.Node = start
22
+ let target: ts.Node = start
23
+
24
+ // Handle the case where we're starting with an identifier directly
25
+ if (ts.isIdentifier(node)) {
26
+ const sym = checker.getSymbolAtLocation(node)
27
+ if (sym) {
28
+ let resolvedSym = sym
29
+ if (resolvedSym.flags & ts.SymbolFlags.Alias) {
30
+ resolvedSym = checker.getAliasedSymbol(resolvedSym) ?? resolvedSym
31
+ }
32
+
33
+ const decls = resolvedSym.declarations ?? []
34
+ if (decls.length > 0) {
35
+ // Start with the declaration, not the reference
36
+ const decl = decls[0]!
37
+
38
+ // If it's a variable declaration with a function initializer, use the function directly
39
+ if (
40
+ ts.isVariableDeclaration(decl) &&
41
+ decl.initializer &&
42
+ (ts.isFunctionExpression(decl.initializer) ||
43
+ ts.isArrowFunction(decl.initializer))
44
+ ) {
45
+ target = decl.initializer
46
+ // Return early - we found the function directly
47
+ const sf = target.getSourceFile()
48
+ const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_')
49
+ const { line, character } = ts.getLineAndCharacterOfPosition(
50
+ sf,
51
+ target.getStart()
52
+ )
53
+ return `pikkuFn_${file}_L${line + 1}C${character + 1}`
54
+ }
55
+ // Otherwise continue resolution with the declaration
56
+ node = decl
57
+ target = decl!
58
+ }
59
+ }
60
+ }
61
+
62
+ // In an object literal property value, first try to resolve the identifier
63
+ if (
64
+ ts.isPropertyAssignment(node.parent) &&
65
+ node === node.parent.initializer &&
66
+ ts.isIdentifier(node)
67
+ ) {
68
+ const sym = checker.getSymbolAtLocation(node)
69
+ if (sym) {
70
+ // Process the symbol to find the real declaration
71
+ let resolvedSym = sym
72
+ if (resolvedSym.flags & ts.SymbolFlags.Alias) {
73
+ resolvedSym = checker.getAliasedSymbol(resolvedSym) ?? resolvedSym
74
+ }
75
+
76
+ const decls = resolvedSym.declarations ?? []
77
+ if (decls.length > 0) {
78
+ // Found a declaration - use it as our new target
79
+ const decl = decls[0]
80
+
81
+ if (!decl) {
82
+ throw new Error('No declaration found')
83
+ }
84
+
85
+ // If it's a variable declaration with an initializer function, use that
86
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
87
+ if (
88
+ ts.isFunctionExpression(decl.initializer) ||
89
+ ts.isArrowFunction(decl.initializer)
90
+ ) {
91
+ target = decl.initializer
92
+ // Return early - we found the function directly
93
+ const sf = target.getSourceFile()
94
+ const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_')
95
+ const { line, character } = ts.getLineAndCharacterOfPosition(
96
+ sf,
97
+ target.getStart()
98
+ )
99
+ return `pikkuFn_${file}_L${line + 1}C${character + 1}`
100
+ }
101
+ } else if (ts.isFunctionDeclaration(decl)) {
102
+ // Already a function declaration
103
+ target = decl
104
+ // Return early
105
+ const sf = target.getSourceFile()
106
+ const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_')
107
+ const { line, character } = ts.getLineAndCharacterOfPosition(
108
+ sf,
109
+ target.getStart()
110
+ )
111
+ return `pikkuFn_${file}_L${line + 1}C${character + 1}`
112
+ }
113
+
114
+ // If we didn't return early, continue with this declaration
115
+ node = decl
116
+ target = decl
117
+ }
118
+ }
119
+ }
120
+
121
+ const seen = new Set<ts.Node>()
122
+ for (let depth = 0; depth < 10; depth++) {
123
+ if (!ts.isIdentifier(node) || seen.has(node)) break
124
+ seen.add(node)
125
+
126
+ let sym = checker.getSymbolAtLocation(node)
127
+ if (!sym) break
128
+ if (sym.flags & ts.SymbolFlags.Alias) {
129
+ sym = checker.getAliasedSymbol(sym) ?? sym
130
+ }
131
+
132
+ const allDecls = sym.declarations ?? []
133
+ // prefer real .ts/.tsx implementation files
134
+ const implDecls = allDecls.filter(
135
+ (d) => !d.getSourceFile().isDeclarationFile
136
+ )
137
+ const decls = implDecls.length ? implDecls : allDecls
138
+
139
+ let didResolve = false
140
+ for (const decl of decls) {
141
+ // 1) direct function foo() {} or function-expression
142
+ if (
143
+ ts.isFunctionDeclaration(decl) ||
144
+ ts.isFunctionExpression(decl) ||
145
+ ts.isArrowFunction(decl)
146
+ ) {
147
+ target = decl
148
+ didResolve = true
149
+ break
150
+ }
151
+
152
+ // 2) const foo = () => {} or foo = function() {}
153
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
154
+ const init = decl.initializer
155
+ if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) {
156
+ target = init
157
+ didResolve = true
158
+ break
159
+ }
160
+ // 2b) const foo = bar; (follow the next identifier)
161
+ if (ts.isIdentifier(init)) {
162
+ node = init
163
+ target = init
164
+ didResolve = true
165
+ break
166
+ }
167
+ }
168
+
169
+ // 3) Handle shorthand property assignments: { foo } (equivalent to { foo: foo })
170
+ if (ts.isShorthandPropertyAssignment(decl)) {
171
+ // Get the symbol for the shorthand property
172
+ const shorthandSym = checker.getShorthandAssignmentValueSymbol(decl)
173
+ if (
174
+ shorthandSym &&
175
+ shorthandSym.declarations &&
176
+ shorthandSym.declarations.length > 0
177
+ ) {
178
+ // Use the first declaration as our new target
179
+ const shorthandDecl = shorthandSym.declarations[0]!
180
+ target = shorthandDecl
181
+
182
+ if (!shorthandDecl) {
183
+ throw new Error('No shorthand declaration found')
184
+ }
185
+
186
+ // Check the type of declaration and extract the appropriate identifier to continue resolving
187
+ if (
188
+ ts.isVariableDeclaration(shorthandDecl) &&
189
+ ts.isIdentifier(shorthandDecl.name)
190
+ ) {
191
+ node = shorthandDecl.name
192
+ didResolve = true
193
+ break
194
+ } else if (
195
+ ts.isFunctionDeclaration(shorthandDecl) &&
196
+ shorthandDecl.name &&
197
+ ts.isIdentifier(shorthandDecl.name)
198
+ ) {
199
+ node = shorthandDecl.name
200
+ didResolve = true
201
+ break
202
+ } else if (
203
+ ts.isParameter(shorthandDecl) &&
204
+ ts.isIdentifier(shorthandDecl.name)
205
+ ) {
206
+ node = shorthandDecl.name
207
+ didResolve = true
208
+ break
209
+ } else if (
210
+ ts.isPropertyDeclaration(shorthandDecl) &&
211
+ ts.isIdentifier(shorthandDecl.name)
212
+ ) {
213
+ node = shorthandDecl.name
214
+ didResolve = true
215
+ break
216
+ } else if (
217
+ ts.isMethodDeclaration(shorthandDecl) &&
218
+ ts.isIdentifier(shorthandDecl.name)
219
+ ) {
220
+ node = shorthandDecl.name
221
+ didResolve = true
222
+ break
223
+ }
224
+ }
225
+ }
226
+
227
+ // 4) Handle method declarations in classes/objects
228
+ if (ts.isMethodDeclaration(decl)) {
229
+ target = decl
230
+ didResolve = true
231
+ break
232
+ }
233
+
234
+ // you can add more cases here if your setup uses imports, etc.
235
+ }
236
+
237
+ if (!didResolve) break
238
+ }
239
+
240
+ const sf = target.getSourceFile()
241
+ const file = sf.fileName.replace(/[^A-Za-z0-9_]/g, '_')
242
+ const { line, character } = ts.getLineAndCharacterOfPosition(
243
+ sf,
244
+ target.getStart()
245
+ )
246
+ return `pikkuFn_${file}_L${line + 1}C${character + 1}`
247
+ }
248
+
249
+ /**
250
+ * Updated function to extract and prioritize function names correctly
251
+ * This function follows the priority:
252
+ * 1. Object with a name property
253
+ * 2. Exported name
254
+ * 3. Fallback to deterministic name
255
+ */
256
+ export function extractFunctionName(
257
+ callExpr: ts.Node,
258
+ checker: ts.TypeChecker
259
+ ): ExtractedFunctionName {
260
+ const parent: any = callExpr.parent
261
+
262
+ // Initialize the result
263
+ const result: ExtractedFunctionName = {
264
+ pikkuFuncName: '', // Will be populated later
265
+ name: '', // This will hold our "best" name based on priority
266
+ exportedName: null,
267
+ functionName: null,
268
+ propertyName: null,
269
+ }
270
+
271
+ // Special case for addHTTPRoute: if this is an identifier within an object literal,
272
+ // it might be coming from the HTTP route handling flow
273
+ if (
274
+ ts.isIdentifier(callExpr) &&
275
+ callExpr.parent &&
276
+ ts.isPropertyAssignment(callExpr.parent)
277
+ ) {
278
+ // Try to handle the special case for HTTP route functions
279
+ const sym = checker.getSymbolAtLocation(callExpr)
280
+ if (sym) {
281
+ let resolvedSym = sym
282
+ if (resolvedSym.flags & ts.SymbolFlags.Alias) {
283
+ resolvedSym = checker.getAliasedSymbol(resolvedSym) ?? resolvedSym
284
+ }
285
+
286
+ const decls = resolvedSym.declarations ?? []
287
+ if (decls.length > 0) {
288
+ const decl = decls[0]!
289
+ // Check if the declaration is a variable that uses pikkuSessionlessFunc
290
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
291
+ if (
292
+ ts.isCallExpression(decl.initializer) &&
293
+ ts.isIdentifier(decl.initializer.expression) &&
294
+ decl.initializer.expression.text.startsWith('pikku')
295
+ ) {
296
+ const args = decl.initializer.arguments
297
+ const firstArg = args[0]
298
+ if (
299
+ firstArg &&
300
+ (ts.isArrowFunction(firstArg) ||
301
+ ts.isFunctionExpression(firstArg))
302
+ ) {
303
+ // Use the function directly for position calculation
304
+ result.pikkuFuncName = makeDeterministicAnonName(
305
+ firstArg,
306
+ checker
307
+ )
308
+
309
+ // Continue with name extraction
310
+ if (ts.isIdentifier(parent.name)) {
311
+ result.propertyName = parent.name.text
312
+ }
313
+
314
+ // Check if the variable is exported
315
+ if (
316
+ ts.isVariableDeclaration(decl) &&
317
+ isNamedExport(decl) &&
318
+ ts.isIdentifier(decl.name)
319
+ ) {
320
+ result.exportedName = decl.name.text
321
+ } else if (ts.isIdentifier(decl.name)) {
322
+ // If not exported, still capture the variable name
323
+ result.functionName = decl.name.text
324
+ }
325
+
326
+ // Apply name priority logic
327
+ populateNameByPriority(result)
328
+ return result
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ // First, figure out what function we're really dealing with
337
+ let mainFunc = callExpr
338
+ let originalCallExpr = callExpr // Keep track of the original call expression for name extraction
339
+
340
+ // For direct pikku function calls where callExpr is the call expression itself
341
+ if (ts.isCallExpression(callExpr)) {
342
+ const { expression, arguments: args } = callExpr
343
+
344
+ // Check if this is a pikku function call (pikkuFunc, pikkuSessionlessFunc, etc)
345
+ if (ts.isIdentifier(expression) && expression.text.startsWith('pikku')) {
346
+ // Check for object with 'name' property in first argument
347
+ const firstArg = args[0]
348
+ if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
349
+ for (const prop of firstArg.properties) {
350
+ if (
351
+ ts.isPropertyAssignment(prop) &&
352
+ ts.isIdentifier(prop.name) &&
353
+ prop.name.text === 'name' &&
354
+ ts.isStringLiteral(prop.initializer)
355
+ ) {
356
+ // Priority 1: Object with name property
357
+ result.functionName = prop.initializer.text
358
+ break
359
+ }
360
+ }
361
+ }
362
+
363
+ // Special handling for pikkuSessionlessFunc pattern - use the arrow function directly
364
+ if (expression.text.startsWith('pikku')) {
365
+ if (args.length > 0) {
366
+ const firstArg = args[0]!
367
+ if (
368
+ ts.isArrowFunction(firstArg) ||
369
+ ts.isFunctionExpression(firstArg)
370
+ ) {
371
+ mainFunc = firstArg // Use the arrow function directly instead of the call expression
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ // Handle object initializer with a func property (for both patterns)
378
+ if (args.length > 0) {
379
+ const firstArg = args[0]
380
+ if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
381
+ // Look for func property in the object
382
+ for (const prop of firstArg.properties) {
383
+ if (
384
+ ts.isPropertyAssignment(prop) &&
385
+ ts.isIdentifier(prop.name) &&
386
+ prop.name.text === 'func'
387
+ ) {
388
+ if (ts.isIdentifier(prop.initializer)) {
389
+ // func: someFunction - resolve the function
390
+ const funcSym = checker.getSymbolAtLocation(prop.initializer)
391
+ if (funcSym) {
392
+ let resolvedFuncSym = funcSym
393
+ if (resolvedFuncSym.flags & ts.SymbolFlags.Alias) {
394
+ resolvedFuncSym =
395
+ checker.getAliasedSymbol(resolvedFuncSym) ?? resolvedFuncSym
396
+ }
397
+
398
+ const funcDecls = resolvedFuncSym.declarations ?? []
399
+ if (funcDecls.length > 0) {
400
+ const funcDecl = funcDecls[0]!
401
+ // Check if it's a pikkuSessionlessFunc
402
+ if (
403
+ ts.isVariableDeclaration(funcDecl) &&
404
+ funcDecl.initializer
405
+ ) {
406
+ if (
407
+ ts.isCallExpression(funcDecl.initializer) &&
408
+ ts.isIdentifier(funcDecl.initializer.expression) &&
409
+ funcDecl.initializer.expression.text.startsWith('pikku')
410
+ ) {
411
+ const funcArgs = funcDecl.initializer.arguments
412
+ const firstArg = funcArgs[0]
413
+ if (
414
+ firstArg &&
415
+ (ts.isArrowFunction(firstArg) ||
416
+ ts.isFunctionExpression(firstArg))
417
+ ) {
418
+ mainFunc = firstArg
419
+
420
+ // Check if the variable is exported
421
+ if (
422
+ isNamedExport(funcDecl) &&
423
+ ts.isIdentifier(funcDecl.name)
424
+ ) {
425
+ result.exportedName = funcDecl.name.text
426
+ } else if (ts.isIdentifier(funcDecl.name)) {
427
+ // If not exported, still capture the variable name
428
+ result.functionName = funcDecl.name.text
429
+ }
430
+
431
+ break
432
+ }
433
+ } else if (
434
+ ts.isFunctionExpression(funcDecl.initializer) ||
435
+ ts.isArrowFunction(funcDecl.initializer)
436
+ ) {
437
+ mainFunc = funcDecl.initializer
438
+
439
+ // Check if the variable is exported
440
+ if (
441
+ isNamedExport(funcDecl) &&
442
+ ts.isIdentifier(funcDecl.name)
443
+ ) {
444
+ result.exportedName = funcDecl.name.text
445
+ } else if (ts.isIdentifier(funcDecl.name)) {
446
+ // If not exported, still capture the variable name
447
+ result.functionName = funcDecl.name.text
448
+ }
449
+
450
+ break
451
+ }
452
+ } else if (ts.isFunctionDeclaration(funcDecl)) {
453
+ mainFunc = funcDecl
454
+
455
+ // Check if the function is exported
456
+ if (
457
+ funcDecl.modifiers?.some(
458
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
459
+ ) &&
460
+ funcDecl.name &&
461
+ ts.isIdentifier(funcDecl.name)
462
+ ) {
463
+ result.exportedName = funcDecl.name.text
464
+ } else if (
465
+ funcDecl.name &&
466
+ ts.isIdentifier(funcDecl.name)
467
+ ) {
468
+ // If not exported, still capture the function name
469
+ result.functionName = funcDecl.name.text
470
+ }
471
+
472
+ break
473
+ }
474
+ }
475
+ } else {
476
+ // If we can't resolve the symbol, use the identifier itself
477
+ mainFunc = prop.initializer
478
+ }
479
+ break
480
+ } else if (
481
+ ts.isFunctionExpression(prop.initializer) ||
482
+ ts.isArrowFunction(prop.initializer)
483
+ ) {
484
+ // func: () => {} or func: function() {} - use directly
485
+ mainFunc = prop.initializer
486
+ break
487
+ }
488
+ } else if (
489
+ ts.isShorthandPropertyAssignment(prop) &&
490
+ ts.isIdentifier(prop.name) &&
491
+ prop.name.text === 'func'
492
+ ) {
493
+ // Handle func shorthand property
494
+ const shorthandSym = checker.getShorthandAssignmentValueSymbol(prop)
495
+ if (
496
+ shorthandSym &&
497
+ shorthandSym.declarations &&
498
+ shorthandSym.declarations.length > 0
499
+ ) {
500
+ const shorthandDecl = shorthandSym.declarations[0]
501
+ if (!shorthandDecl) {
502
+ throw new Error('No shorthand declaration found')
503
+ }
504
+ if (
505
+ ts.isVariableDeclaration(shorthandDecl) &&
506
+ shorthandDecl.initializer
507
+ ) {
508
+ if (
509
+ ts.isCallExpression(shorthandDecl.initializer) &&
510
+ ts.isIdentifier(shorthandDecl.initializer.expression) &&
511
+ shorthandDecl.initializer.expression.text.startsWith('pikku')
512
+ ) {
513
+ const args = shorthandDecl.initializer.arguments
514
+ const firstArg = args[0]
515
+ if (
516
+ firstArg &&
517
+ (ts.isArrowFunction(firstArg) ||
518
+ ts.isFunctionExpression(firstArg))
519
+ ) {
520
+ mainFunc = firstArg
521
+
522
+ // Check if the variable is exported
523
+ if (
524
+ isNamedExport(shorthandDecl) &&
525
+ ts.isIdentifier(shorthandDecl.name)
526
+ ) {
527
+ result.exportedName = shorthandDecl.name.text
528
+ } else if (ts.isIdentifier(shorthandDecl.name)) {
529
+ // If not exported, still capture the variable name
530
+ result.functionName = shorthandDecl.name.text
531
+ }
532
+
533
+ break
534
+ }
535
+ } else if (
536
+ ts.isFunctionExpression(shorthandDecl.initializer) ||
537
+ ts.isArrowFunction(shorthandDecl.initializer)
538
+ ) {
539
+ mainFunc = shorthandDecl.initializer
540
+
541
+ // Check if the variable is exported
542
+ if (
543
+ isNamedExport(shorthandDecl) &&
544
+ ts.isIdentifier(shorthandDecl.name)
545
+ ) {
546
+ result.exportedName = shorthandDecl.name.text
547
+ } else if (ts.isIdentifier(shorthandDecl.name)) {
548
+ // If not exported, still capture the variable name
549
+ result.functionName = shorthandDecl.name.text
550
+ }
551
+
552
+ break
553
+ }
554
+ } else if (ts.isFunctionDeclaration(shorthandDecl)) {
555
+ mainFunc = shorthandDecl
556
+
557
+ // Check if the function is exported
558
+ if (
559
+ shorthandDecl.modifiers?.some(
560
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
561
+ ) &&
562
+ shorthandDecl.name &&
563
+ ts.isIdentifier(shorthandDecl.name)
564
+ ) {
565
+ result.exportedName = shorthandDecl.name.text
566
+ } else if (
567
+ shorthandDecl.name &&
568
+ ts.isIdentifier(shorthandDecl.name)
569
+ ) {
570
+ // If not exported, still capture the function name
571
+ result.functionName = shorthandDecl.name.text
572
+ }
573
+
574
+ break
575
+ }
576
+ }
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+ // Handle direct identifier case
583
+ else if (ts.isIdentifier(callExpr)) {
584
+ const sym = checker.getSymbolAtLocation(callExpr)
585
+ if (sym) {
586
+ let resolvedSym = sym
587
+ if (resolvedSym.flags & ts.SymbolFlags.Alias) {
588
+ resolvedSym = checker.getAliasedSymbol(resolvedSym) ?? resolvedSym
589
+ }
590
+
591
+ const decls = resolvedSym.declarations ?? []
592
+ if (decls.length > 0) {
593
+ const decl = decls[0]
594
+ if (!decl) {
595
+ throw new Error('No declaration found')
596
+ }
597
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
598
+ if (
599
+ ts.isCallExpression(decl.initializer) &&
600
+ ts.isIdentifier(decl.initializer.expression) &&
601
+ decl.initializer.expression.text.startsWith('pikku')
602
+ ) {
603
+ // Check for object with 'name' property in first argument
604
+ const firstArg = decl.initializer.arguments[0]
605
+ if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
606
+ for (const prop of firstArg.properties) {
607
+ if (
608
+ ts.isPropertyAssignment(prop) &&
609
+ ts.isIdentifier(prop.name) &&
610
+ prop.name.text === 'name' &&
611
+ ts.isStringLiteral(prop.initializer)
612
+ ) {
613
+ // Priority 1: Object with name property
614
+ result.functionName = prop.initializer.text
615
+ break
616
+ }
617
+ }
618
+ }
619
+
620
+ if (decl.initializer.expression.text.startsWith('pikku')) {
621
+ if (
622
+ firstArg &&
623
+ (ts.isArrowFunction(firstArg) ||
624
+ ts.isFunctionExpression(firstArg))
625
+ ) {
626
+ mainFunc = firstArg
627
+ }
628
+ }
629
+
630
+ // Check if the variable is exported
631
+ if (isNamedExport(decl) && ts.isIdentifier(decl.name)) {
632
+ result.exportedName = decl.name.text
633
+ } else if (ts.isIdentifier(decl.name)) {
634
+ // If not explicitly set by name property above, set functionName
635
+ if (!result.functionName) {
636
+ result.functionName = decl.name.text
637
+ }
638
+ }
639
+ } else if (
640
+ ts.isFunctionExpression(decl.initializer) ||
641
+ ts.isArrowFunction(decl.initializer)
642
+ ) {
643
+ mainFunc = decl.initializer
644
+
645
+ // Check if the variable is exported
646
+ if (isNamedExport(decl) && ts.isIdentifier(decl.name)) {
647
+ result.exportedName = decl.name.text
648
+ } else if (ts.isIdentifier(decl.name)) {
649
+ result.functionName = decl.name.text
650
+ }
651
+ }
652
+ } else if (ts.isFunctionDeclaration(decl)) {
653
+ mainFunc = decl
654
+
655
+ // Check if the function is exported
656
+ if (
657
+ decl.modifiers?.some(
658
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
659
+ ) &&
660
+ decl.name &&
661
+ ts.isIdentifier(decl.name)
662
+ ) {
663
+ result.exportedName = decl.name.text
664
+ } else if (decl.name && ts.isIdentifier(decl.name)) {
665
+ result.functionName = decl.name.text
666
+ }
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ // Now generate the deterministic function name based on the resolved function
673
+ result.pikkuFuncName = makeDeterministicAnonName(mainFunc, checker)
674
+
675
+ // Continue with regular name extraction for remaining cases
676
+ // 1) const foo = pikkuFunc(...)
677
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
678
+ if (isNamedExport(parent)) {
679
+ result.exportedName = parent.name.text
680
+ } else {
681
+ // Still capture the variable name even if not exported
682
+ result.functionName = parent.name.text
683
+ }
684
+ }
685
+ // 2) { foo: pikkuFunc(...) }
686
+ else if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) {
687
+ result.propertyName = parent.name.text
688
+ }
689
+ // 2b) Handle shorthand property { foo } - which is equivalent to { foo: foo }
690
+ else if (
691
+ ts.isShorthandPropertyAssignment(parent) &&
692
+ ts.isIdentifier(parent.name)
693
+ ) {
694
+ result.propertyName = parent.name.text
695
+ }
696
+ // 3) Handle any remaining cases for pikkuFunc({ name: '…', func: … })
697
+ else if (ts.isCallExpression(originalCallExpr)) {
698
+ const firstArg = originalCallExpr.arguments[0]
699
+ if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
700
+ for (const prop of firstArg.properties) {
701
+ if (
702
+ ts.isPropertyAssignment(prop) &&
703
+ ts.isIdentifier(prop.name) &&
704
+ prop.name.text === 'name' &&
705
+ ts.isStringLiteral(prop.initializer) &&
706
+ !result.functionName // Only set if not already set
707
+ ) {
708
+ result.functionName = prop.initializer.text
709
+ break
710
+ }
711
+ }
712
+ }
713
+ }
714
+
715
+ // Apply name priority logic
716
+ populateNameByPriority(result)
717
+ return result
718
+ }
719
+
720
+ /**
721
+ * Helper function to populate the 'name' field based on priority
722
+ */
723
+ function populateNameByPriority(result: ExtractedFunctionName): void {
724
+ // Priority 1: If we have a functionName (from name property or variable name), use that
725
+ if (result.functionName) {
726
+ result.name = result.functionName
727
+ }
728
+ // Priority 2: If we have an exported name, use that
729
+ else if (result.exportedName) {
730
+ result.name = result.exportedName
731
+ }
732
+ // Priority 3: If we have a property name, use that
733
+ else if (result.propertyName) {
734
+ result.name = result.propertyName
735
+ }
736
+ // Fallback: Use the deterministic name, but we could shorten it in the future
737
+ else {
738
+ // For now, just use the full pikkuFuncName
739
+ result.name = result.pikkuFuncName
740
+
741
+ // Alternative: extract just the filename and line/column from pikkuFuncName
742
+ // const nameParts = result.pikkuFuncName.split('_');
743
+ // if (nameParts.length >= 3) {
744
+ // // Extract just filename + line/column info
745
+ // result.name = `${nameParts[1]}_${nameParts[2]}`;
746
+ // }
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Helper function to check if a variable declaration is a named export
752
+ */
753
+ function isNamedExport(declaration: ts.VariableDeclaration): boolean {
754
+ let parent: any = declaration.parent
755
+ if (!parent) return false
756
+
757
+ // Check if it's part of a variable declaration list
758
+ if (ts.isVariableDeclarationList(parent)) {
759
+ parent = parent.parent
760
+ if (!parent) return false
761
+
762
+ // Check if it's in an export declaration
763
+ if (ts.isVariableStatement(parent)) {
764
+ return (
765
+ parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ??
766
+ false
767
+ )
768
+ }
769
+ }
770
+
771
+ return false
772
+ }
773
+
774
+ // Until here
775
+ export const extractTypeKeys = (type: ts.Type): string[] => {
776
+ return type.getProperties().map((symbol) => symbol.getName())
777
+ }
778
+
779
+ export function getPropertyAssignmentInitializer(
780
+ obj: ts.ObjectLiteralExpression,
781
+ propName: string,
782
+ followShorthand = false,
783
+ checker?: ts.TypeChecker
784
+ ): ts.Expression | undefined {
785
+ for (const prop of obj.properties) {
786
+ // ① foo: () => {}
787
+ if (
788
+ ts.isPropertyAssignment(prop) &&
789
+ ts.isIdentifier(prop.name) &&
790
+ prop.name.text === propName
791
+ ) {
792
+ return prop.initializer
793
+ }
794
+
795
+ // ② foo() { … }
796
+ if (
797
+ ts.isMethodDeclaration(prop) &&
798
+ ts.isIdentifier(prop.name) &&
799
+ prop.name.text === propName
800
+ ) {
801
+ return prop.name // the method node *is* the function
802
+ }
803
+
804
+ // ③ { foo } (shorthand)
805
+ if (
806
+ followShorthand &&
807
+ ts.isShorthandPropertyAssignment(prop) &&
808
+ prop.name.text === propName
809
+ ) {
810
+ if (!checker) return prop.name // best effort without a checker
811
+
812
+ let sym = checker.getSymbolAtLocation(prop.name)
813
+ if (sym && sym.flags & ts.SymbolFlags.Alias) {
814
+ sym = checker.getAliasedSymbol(sym)
815
+ }
816
+
817
+ const decl = sym?.declarations?.[0]
818
+
819
+ // const foo = () => {}
820
+ if (
821
+ decl &&
822
+ ts.isVariableDeclaration(decl) &&
823
+ decl.initializer &&
824
+ (ts.isArrowFunction(decl.initializer) ||
825
+ ts.isFunctionExpression(decl.initializer))
826
+ ) {
827
+ return decl.initializer
828
+ }
829
+
830
+ // function foo() {}
831
+ if (
832
+ decl &&
833
+ (ts.isFunctionDeclaration(decl) ||
834
+ ts.isArrowFunction(decl) ||
835
+ ts.isFunctionExpression(decl))
836
+ ) {
837
+ return decl as ts.Expression
838
+ }
839
+
840
+ // fallback – just give back the identifier
841
+ return prop.name
842
+ }
843
+ }
844
+
845
+ return undefined
846
+ }
847
+
848
+ export const matchesFilters = (
849
+ filters: InspectorFilters,
850
+ params: { tags?: string[] },
851
+ meta: { type: 'schedule' | 'http' | 'channel'; name: string }
852
+ ) => {
853
+ if (Object.keys(filters).length === 0 || filters.tags?.length === 0) {
854
+ return true
855
+ }
856
+
857
+ if (filters.tags?.some((tag) => params.tags?.includes(tag))) {
858
+ return true
859
+ }
860
+
861
+ console.debug(`⒡ Filtered: ${meta.type}:${meta.name}`)
862
+ return false
863
+ }