@pikku/inspector 0.6.4 → 0.7.1

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 CHANGED
@@ -1,356 +1,848 @@
1
1
  import * as ts from 'typescript'
2
- import { TypesMap } from './types-map.js'
3
2
  import { InspectorFilters } from './types.js'
4
3
 
5
- type FunctionTypes = {
6
- type: string | null
7
- inputTypes: ts.Type[]
8
- inputs: null | string[]
9
- outputTypes: ts.Type[]
10
- outputs: null | string[]
4
+ type ExtractedFunctionName = {
5
+ pikkuFuncName: string
6
+ name: string
7
+ exportedName: string | null
8
+ functionName: string | null
9
+ propertyName: string | null
11
10
  }
12
11
 
13
- export const extractTypeKeys = (type: ts.Type): string[] => {
14
- return type.getProperties().map((symbol) => symbol.getName())
15
- }
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
+ }
16
32
 
17
- export const nullifyTypes = (type: string | null) => {
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
18
63
  if (
19
- type === 'void' ||
20
- type === 'undefined' ||
21
- type === 'unknown' ||
22
- type === 'any'
64
+ ts.isPropertyAssignment(node.parent) &&
65
+ node === node.parent.initializer &&
66
+ ts.isIdentifier(node)
23
67
  ) {
24
- return null
25
- }
26
- return type
27
- }
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
+ }
28
75
 
29
- const isValidVariableName = (name: string) => {
30
- const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
31
- return regex.test(name)
32
- }
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]
33
80
 
34
- export const getNamesAndTypes = (
35
- checker: ts.TypeChecker,
36
- typesMap: TypesMap,
37
- direction: 'Input' | 'Output',
38
- funcName: string,
39
- type: ts.Type
40
- ) => {
41
- const result: {
42
- names: Set<string>
43
- types: ts.Type[]
44
- } = {
45
- names: new Set(),
46
- types: [],
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
+ }
47
119
  }
48
120
 
49
- const { names, types } = resolveUnionTypes(checker, type)
50
- const firstName = names[0]
51
- if (names.length > 1 || (firstName && !isValidVariableName(firstName))) {
52
- const aliasType = names.join(' | ')
53
- const aliasName = `${funcName.charAt(0).toUpperCase()}${funcName.slice(1)}${direction}`
54
-
55
- result.names = new Set([aliasName])
56
- result.types = types
57
-
58
- const references = types
59
- .map((t) => resolveTypeImports(t, typesMap, true))
60
- .flat()
61
- typesMap.addCustomType(aliasName, aliasType, references)
62
- } else {
63
- const uniqueNames = names
64
- .map((name, i) => {
65
- const type = types[i]
66
- if (!type) {
67
- throw new Error('TODO: Expected a type here to match name')
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
68
159
  }
69
- if (isPrimitiveType(type)) {
70
- return name
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
71
166
  }
72
- return resolveTypeImports(type, typesMap, false)
73
- })
74
- .flat()
75
- result.names = new Set(uniqueNames)
76
- result.types = types
77
- }
167
+ }
78
168
 
79
- return {
80
- names: Array.from(result.names),
81
- types: result.types,
82
- }
83
- }
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
84
181
 
85
- export const isPrimitiveType = (type: ts.Type): boolean => {
86
- const primitiveFlags =
87
- ts.TypeFlags.Number |
88
- ts.TypeFlags.String |
89
- ts.TypeFlags.Boolean |
90
- ts.TypeFlags.BigInt |
91
- ts.TypeFlags.ESSymbol |
92
- ts.TypeFlags.Void |
93
- ts.TypeFlags.Undefined |
94
- ts.TypeFlags.Null |
95
- ts.TypeFlags.Any |
96
- ts.TypeFlags.Unknown
97
-
98
- return (type.flags & primitiveFlags) !== 0
99
- }
182
+ if (!shorthandDecl) {
183
+ throw new Error('No shorthand declaration found')
184
+ }
100
185
 
101
- export const resolveUnionTypes = (
102
- checker: ts.TypeChecker,
103
- type: ts.Type
104
- ): { types: ts.Type[]; names: string[] } => {
105
- const types: ts.Type[] = []
106
- const names: string[] = []
107
-
108
- // Check if it's a union type AND not part of an intersection
109
- if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
110
- for (const t of type.types) {
111
- const name = nullifyTypes(checker.typeToString(t))
112
- if (name) {
113
- types.push(t)
114
- names.push(name)
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
+ }
115
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.
116
235
  }
117
- } else {
118
- const name = nullifyTypes(checker.typeToString(type))
119
- if (name) {
120
- types.push(type)
121
- names.push(name)
122
- }
236
+
237
+ if (!didResolve) break
123
238
  }
124
239
 
125
- return { types, names }
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}`
126
247
  }
127
248
 
128
- export const resolveTypeImports = (
129
- type: ts.Type,
130
- resolvedTypes: TypesMap,
131
- isCustom: boolean
132
- ): string[] => {
133
- const types: string[] = []
134
-
135
- const visitType = (currentType: ts.Type) => {
136
- const symbol = currentType.aliasSymbol || currentType.getSymbol()
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
+ }
137
270
 
138
- if (symbol) {
139
- const declarations = symbol.getDeclarations()
140
- const declaration = declarations?.[0]
141
- if (declaration) {
142
- const sourceFile = declaration.getSourceFile()
143
- const path = sourceFile.fileName
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
+ }
144
285
 
145
- // Skip built-in utility types or TypeScript lib types
146
- if (
147
- !path.includes('node_modules/typescript') &&
148
- symbol.getName() !== '__type' &&
149
- !isPrimitiveType(currentType)
150
- ) {
151
- const originalName = symbol.getName()
152
- // Check if the type is already in the map
153
- let uniqueName = resolvedTypes.exists(originalName, path)
154
- if (!uniqueName) {
155
- if (isCustom) {
156
- uniqueName = resolvedTypes.addUniqueType(originalName, path)
157
- } else {
158
- resolvedTypes.addType(originalName, path)
159
- uniqueName = originalName
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
160
329
  }
161
330
  }
162
- types.push(uniqueName)
163
331
  }
164
332
  }
165
333
  }
334
+ }
166
335
 
167
- if (isCustom) {
168
- // Handle nested utility types like Partial, Pick, etc.
169
- if (currentType.aliasTypeArguments) {
170
- currentType.aliasTypeArguments.forEach(visitType)
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
+ }
171
361
  }
172
362
 
173
- // Handle intersections and unions
174
- if (currentType.isUnionOrIntersection()) {
175
- currentType.types.forEach(visitType)
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
+ }
176
374
  }
375
+ }
177
376
 
178
- // Handle object types with type arguments
179
- if (
180
- currentType.flags & ts.TypeFlags.Object &&
181
- (currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
182
- ) {
183
- const typeRef = currentType as ts.TypeReference
184
- typeRef.typeArguments?.forEach(visitType)
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
+ }
185
579
  }
186
580
  }
187
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
+ }
188
590
 
189
- visitType(type)
190
- return types
191
- }
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
+ }
192
619
 
193
- export const getPropertyAssignment = (
194
- obj: ts.ObjectLiteralExpression,
195
- name: string
196
- ) => {
197
- const property = obj.properties.find(
198
- (p) =>
199
- (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
200
- ts.isIdentifier(p.name) &&
201
- p.name.text === name
202
- )
203
- if (!property) {
204
- console.error(`Missing property '${name}' in object`)
205
- return null
206
- }
207
- return property
208
- }
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
+ }
209
629
 
210
- export const getTypeArgumentsOfType = (
211
- checker: ts.TypeChecker,
212
- type: ts.Type
213
- ): readonly ts.Type[] | null => {
214
- if (type.isUnionOrIntersection()) {
215
- const types: ts.Type[] = []
216
- for (const subType of type.types) {
217
- const subTypeArgs = getTypeArgumentsOfType(checker, subType)
218
- if (subTypeArgs) {
219
- types.push(...subTypeArgs)
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
+ }
220
668
  }
221
669
  }
222
- return types.length > 0 ? types : null
223
670
  }
224
671
 
225
- // If the type is a TypeReference with typeArguments, return them
226
- if (
227
- type.flags & ts.TypeFlags.Object &&
228
- (type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
229
- ) {
230
- const typeRef = type as ts.TypeReference
231
- if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
232
- return typeRef.typeArguments
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
233
683
  }
234
684
  }
235
-
236
- // If the type is an alias with aliasTypeArguments, return them
237
- if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
238
- return type.aliasTypeArguments as ts.Type[]
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
+ }
239
713
  }
240
714
 
241
- return null
715
+ // Apply name priority logic
716
+ populateNameByPriority(result)
717
+ return result
242
718
  }
243
719
 
244
- export const getFunctionTypes = (
245
- checker: ts.TypeChecker,
246
- obj: ts.ObjectLiteralExpression,
247
- {
248
- typesMap,
249
- funcName,
250
- subFunctionName = funcName,
251
- inputIndex,
252
- outputIndex,
253
- }: {
254
- typesMap: TypesMap
255
- subFunctionName?: string
256
- funcName: string
257
- inputIndex: number
258
- outputIndex: number
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
259
727
  }
260
- ): FunctionTypes => {
261
- const result: FunctionTypes = {
262
- inputTypes: [],
263
- inputs: null,
264
- outputTypes: [],
265
- outputs: null,
266
- type: null,
728
+ // Priority 2: If we have an exported name, use that
729
+ else if (result.exportedName) {
730
+ result.name = result.exportedName
267
731
  }
268
-
269
- const property = getPropertyAssignment(obj, subFunctionName)
270
- if (!property) {
271
- return result
732
+ // Priority 3: If we have a property name, use that
733
+ else if (result.propertyName) {
734
+ result.name = result.propertyName
272
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
+ }
273
749
 
274
- let type: ts.Type | undefined
275
-
276
- // Handle shorthand property assignment
277
- if (ts.isShorthandPropertyAssignment(property)) {
278
- const symbol = checker.getShorthandAssignmentValueSymbol(property)
279
- if (symbol) {
280
- type = checker.getTypeOfSymbolAtLocation(symbol, property)
281
- if (funcName === 'func') {
282
- funcName = symbol.name
283
- }
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
+ )
284
768
  }
285
769
  }
286
- // Handle regular property assignment
287
- else if (ts.isPropertyAssignment(property)) {
288
- if (ts.isObjectLiteralExpression(property.initializer)) {
289
- return getFunctionTypes(checker, property.initializer, {
290
- typesMap,
291
- funcName,
292
- subFunctionName: 'func',
293
- inputIndex,
294
- outputIndex,
295
- })
296
- }
297
770
 
298
- if (property.initializer) {
299
- type = checker.getTypeAtLocation(property.initializer)
300
- if (funcName === 'func') {
301
- funcName = property.initializer.getText()
302
- }
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
303
793
  }
304
- }
305
794
 
306
- if (!type) {
307
- console.error(`Unable to resolve type for property '${funcName}'`)
308
- return result
309
- }
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
+ }
310
803
 
311
- result.type = type.aliasSymbol?.getEscapedName() || null
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
+ }
312
816
 
313
- // Access type arguments from TypeReference
314
- const typeArguments = getTypeArgumentsOfType(checker, type)
817
+ const decl = sym?.declarations?.[0]
315
818
 
316
- if (!typeArguments || typeArguments.length === 0) {
317
- // This is the case for inline functions. In this case we would want to
318
- // get the types from the second argument of the function...
319
- console.error(
320
- `\x1b[31m• No generic type arguments found for ${funcName}. Support for inline functions is not yet implemented.\x1b[0m`
321
- )
322
- return result
323
- }
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
+ }
324
829
 
325
- if (inputIndex !== undefined && inputIndex < typeArguments.length) {
326
- const { names, types } = getNamesAndTypes(
327
- checker,
328
- typesMap,
329
- 'Input',
330
- funcName,
331
- typeArguments[inputIndex]!
332
- )
333
- result.inputs = names
334
- result.inputTypes = types
335
- } else {
336
- console.log(`No input defined for ${funcName}`)
337
- }
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
+ }
338
839
 
339
- if (outputIndex !== undefined && outputIndex < typeArguments.length) {
340
- const { names, types } = getNamesAndTypes(
341
- checker,
342
- typesMap,
343
- 'Output',
344
- funcName,
345
- typeArguments[outputIndex]!
346
- )
347
- result.outputs = names
348
- result.outputTypes = types
349
- } else {
350
- console.info(`No output defined for ${funcName}`)
840
+ // fallback just give back the identifier
841
+ return prop.name
842
+ }
351
843
  }
352
844
 
353
- return result
845
+ return undefined
354
846
  }
355
847
 
356
848
  export const matchesFilters = (