fcis 0.1.0

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.
Files changed (151) hide show
  1. package/.plans/001-fcis-analyzer.md +832 -0
  2. package/.plans/002-fcis-analyzer-improvements.md +205 -0
  3. package/README.md +272 -0
  4. package/TECHNICAL.md +386 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +1836 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +709 -0
  9. package/dist/index.js +1845 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +47 -0
  12. package/pnpm-workspace.yaml +0 -0
  13. package/src/analyzer.ts +266 -0
  14. package/src/classification/classifier.ts +156 -0
  15. package/src/classification/derive-status.ts +171 -0
  16. package/src/classification/quality-scorer.ts +481 -0
  17. package/src/cli.ts +286 -0
  18. package/src/detection/detect-markers.ts +480 -0
  19. package/src/detection/markers.ts +332 -0
  20. package/src/extraction/extract-functions.ts +570 -0
  21. package/src/extraction/extractor.ts +188 -0
  22. package/src/index.ts +111 -0
  23. package/src/reporting/report-console.ts +416 -0
  24. package/src/reporting/report-json.ts +232 -0
  25. package/src/scoring/scorer.ts +504 -0
  26. package/src/types.ts +248 -0
  27. package/tests/classifier.test.ts +480 -0
  28. package/tests/derive-status.test.ts +464 -0
  29. package/tests/detect-markers.test.ts +639 -0
  30. package/tests/extractor.test.ts +155 -0
  31. package/tests/integration.test.ts +706 -0
  32. package/tests/quality-scorer.test.ts +650 -0
  33. package/tests/scorer.test.ts +768 -0
  34. package/tsconfig.json +34 -0
  35. package/tsup.config.ts +17 -0
  36. package/vendor/ts-morph/.editorconfig +10 -0
  37. package/vendor/ts-morph/.gitattributes +11 -0
  38. package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
  39. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  40. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
  41. package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  42. package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
  43. package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
  44. package/vendor/ts-morph/.vscode/settings.json +10 -0
  45. package/vendor/ts-morph/CONTRIBUTING.md +23 -0
  46. package/vendor/ts-morph/DEVELOPMENT.md +32 -0
  47. package/vendor/ts-morph/LICENSE +21 -0
  48. package/vendor/ts-morph/deno.json +8 -0
  49. package/vendor/ts-morph/deno.lock +1233 -0
  50. package/vendor/ts-morph/docs/CNAME +1 -0
  51. package/vendor/ts-morph/docs/Gemfile +2 -0
  52. package/vendor/ts-morph/docs/_config.yml +5 -0
  53. package/vendor/ts-morph/docs/_layouts/default.html +159 -0
  54. package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
  55. package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
  56. package/vendor/ts-morph/docs/details/ambient.md +38 -0
  57. package/vendor/ts-morph/docs/details/async.md +31 -0
  58. package/vendor/ts-morph/docs/details/classes.md +314 -0
  59. package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
  60. package/vendor/ts-morph/docs/details/comments.md +122 -0
  61. package/vendor/ts-morph/docs/details/decorators.md +119 -0
  62. package/vendor/ts-morph/docs/details/documentation.md +73 -0
  63. package/vendor/ts-morph/docs/details/enums.md +117 -0
  64. package/vendor/ts-morph/docs/details/exports.md +308 -0
  65. package/vendor/ts-morph/docs/details/expressions.md +46 -0
  66. package/vendor/ts-morph/docs/details/functions.md +150 -0
  67. package/vendor/ts-morph/docs/details/generators.md +27 -0
  68. package/vendor/ts-morph/docs/details/identifiers.md +79 -0
  69. package/vendor/ts-morph/docs/details/imports.md +191 -0
  70. package/vendor/ts-morph/docs/details/index.md +52 -0
  71. package/vendor/ts-morph/docs/details/initializers.md +40 -0
  72. package/vendor/ts-morph/docs/details/interfaces.md +218 -0
  73. package/vendor/ts-morph/docs/details/literals.md +20 -0
  74. package/vendor/ts-morph/docs/details/modifiers.md +38 -0
  75. package/vendor/ts-morph/docs/details/modules.md +113 -0
  76. package/vendor/ts-morph/docs/details/namespaces.md +7 -0
  77. package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
  78. package/vendor/ts-morph/docs/details/parameters.md +64 -0
  79. package/vendor/ts-morph/docs/details/signatures.md +41 -0
  80. package/vendor/ts-morph/docs/details/source-files.md +292 -0
  81. package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
  82. package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
  83. package/vendor/ts-morph/docs/details/types.md +254 -0
  84. package/vendor/ts-morph/docs/details/variables.md +110 -0
  85. package/vendor/ts-morph/docs/emitting.md +151 -0
  86. package/vendor/ts-morph/docs/index.md +25 -0
  87. package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
  88. package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
  89. package/vendor/ts-morph/docs/manipulation/index.md +136 -0
  90. package/vendor/ts-morph/docs/manipulation/order.md +14 -0
  91. package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
  92. package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
  93. package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
  94. package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
  95. package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
  96. package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
  97. package/vendor/ts-morph/docs/metrics/performance.json +4 -0
  98. package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
  99. package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
  100. package/vendor/ts-morph/docs/navigation/directories.md +287 -0
  101. package/vendor/ts-morph/docs/navigation/example.md +50 -0
  102. package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
  103. package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
  104. package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
  105. package/vendor/ts-morph/docs/navigation/index.md +94 -0
  106. package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
  107. package/vendor/ts-morph/docs/navigation/program.md +25 -0
  108. package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
  109. package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
  110. package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
  111. package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
  112. package/vendor/ts-morph/docs/setup/file-system.md +106 -0
  113. package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
  114. package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
  115. package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
  116. package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
  117. package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
  118. package/vendor/ts-morph/docs/setup/index.md +94 -0
  119. package/vendor/ts-morph/docs/utilities.md +55 -0
  120. package/vendor/ts-morph/dprint.json +23 -0
  121. package/vendor/ts-morph/package.json +30 -0
  122. package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
  123. package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
  124. package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
  125. package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
  126. package/vendor/ts-morph/packages/common/LICENSE +21 -0
  127. package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
  128. package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
  129. package/vendor/ts-morph/packages/common/package.json +65 -0
  130. package/vendor/ts-morph/packages/common/readme.md +5 -0
  131. package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
  132. package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
  133. package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
  134. package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
  135. package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
  136. package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
  137. package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
  138. package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
  139. package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
  140. package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
  141. package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
  142. package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
  143. package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
  144. package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
  145. package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
  146. package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
  147. package/vendor/ts-morph/readme.md +14 -0
  148. package/vendor/ts-morph/rfcs/README.md +13 -0
  149. package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
  150. package/vendor/ts-morph/tsconfig.common.json +17 -0
  151. package/vitest.config.ts +16 -0
@@ -0,0 +1,570 @@
1
+ /**
2
+ * AST Function Extraction
3
+ *
4
+ * Extracts all function-like nodes from a TypeScript source file into
5
+ * normalized ExtractedFunction structures. This is part of the SHELL layer
6
+ * as it interacts with ts-morph AST nodes.
7
+ *
8
+ * Handles all function forms:
9
+ * - FunctionDeclaration
10
+ * - MethodDeclaration
11
+ * - ArrowFunction (including those passed as arguments)
12
+ * - FunctionExpression (including those passed as arguments)
13
+ * - GetAccessorDeclaration
14
+ * - SetAccessorDeclaration
15
+ */
16
+
17
+ import {
18
+ type ArrowFunction,
19
+ type CallExpression,
20
+ type FunctionDeclaration,
21
+ type FunctionExpression,
22
+ type GetAccessorDeclaration,
23
+ type MethodDeclaration,
24
+ type Node,
25
+ type PropertyAccessExpression,
26
+ type SetAccessorDeclaration,
27
+ type SourceFile,
28
+ SyntaxKind,
29
+ } from 'ts-morph'
30
+
31
+ import type { CallSite, ExtractedFunction, FileImports } from '../types.js'
32
+
33
+ type FunctionLikeNode =
34
+ | FunctionDeclaration
35
+ | MethodDeclaration
36
+ | ArrowFunction
37
+ | FunctionExpression
38
+ | GetAccessorDeclaration
39
+ | SetAccessorDeclaration
40
+
41
+ /**
42
+ * Extract all functions from a source file
43
+ */
44
+ export function extractFunctions(sourceFile: SourceFile): ExtractedFunction[] {
45
+ const functions: ExtractedFunction[] = []
46
+ const filePath = sourceFile.getFilePath()
47
+ // Track already-added functions by their position to avoid duplicates
48
+ const addedPositions = new Set<string>()
49
+
50
+ const addFunction = (fn: ExtractedFunction) => {
51
+ const posKey = `${fn.startLine}:${fn.endLine}`
52
+ if (!addedPositions.has(posKey)) {
53
+ addedPositions.add(posKey)
54
+ functions.push(fn)
55
+ }
56
+ }
57
+
58
+ // Get all function declarations
59
+ for (const func of sourceFile.getFunctions()) {
60
+ addFunction(extractFunctionData(func, filePath, 'function'))
61
+ }
62
+
63
+ // Get all class methods, getters, and setters
64
+ for (const classDecl of sourceFile.getClasses()) {
65
+ const className = classDecl.getName() ?? null
66
+
67
+ for (const method of classDecl.getMethods()) {
68
+ addFunction(extractFunctionData(method, filePath, 'method', className))
69
+ }
70
+
71
+ for (const getter of classDecl.getGetAccessors()) {
72
+ addFunction(extractFunctionData(getter, filePath, 'getter', className))
73
+ }
74
+
75
+ for (const setter of classDecl.getSetAccessors()) {
76
+ addFunction(extractFunctionData(setter, filePath, 'setter', className))
77
+ }
78
+ }
79
+
80
+ // Get arrow functions and function expressions from variable declarations
81
+ // These get named after their variable
82
+ for (const varStatement of sourceFile.getVariableStatements()) {
83
+ const isExported = varStatement.isExported()
84
+
85
+ for (const decl of varStatement.getDeclarations()) {
86
+ const initializer = decl.getInitializer()
87
+ const varName = decl.getName()
88
+
89
+ if (initializer) {
90
+ if (initializer.getKind() === SyntaxKind.ArrowFunction) {
91
+ addFunction(
92
+ extractFunctionData(
93
+ initializer as ArrowFunction,
94
+ filePath,
95
+ 'arrow',
96
+ varName,
97
+ isExported,
98
+ ),
99
+ )
100
+ } else if (initializer.getKind() === SyntaxKind.FunctionExpression) {
101
+ addFunction(
102
+ extractFunctionData(
103
+ initializer as FunctionExpression,
104
+ filePath,
105
+ 'function-expression',
106
+ varName,
107
+ isExported,
108
+ ),
109
+ )
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // Also check for exported default arrow functions/function expressions
116
+ const defaultExport = sourceFile.getDefaultExportSymbol()
117
+ if (defaultExport) {
118
+ const declarations = defaultExport.getDeclarations()
119
+ for (const decl of declarations) {
120
+ if (decl.getKind() === SyntaxKind.ArrowFunction) {
121
+ addFunction(
122
+ extractFunctionData(
123
+ decl as ArrowFunction,
124
+ filePath,
125
+ 'arrow',
126
+ 'default',
127
+ true,
128
+ ),
129
+ )
130
+ } else if (decl.getKind() === SyntaxKind.FunctionExpression) {
131
+ addFunction(
132
+ extractFunctionData(
133
+ decl as FunctionExpression,
134
+ filePath,
135
+ 'function-expression',
136
+ 'default',
137
+ true,
138
+ ),
139
+ )
140
+ }
141
+ }
142
+ }
143
+
144
+ // Extract ALL arrow functions and function expressions in the file,
145
+ // including those passed as arguments (e.g., tRPC handlers, callbacks, React components)
146
+ // This catches patterns like: .mutation(async ({ ctx }) => { ... })
147
+ sourceFile.forEachDescendant(node => {
148
+ if (node.getKind() === SyntaxKind.ArrowFunction) {
149
+ const arrowFn = node as ArrowFunction
150
+ const parentContext = inferParentContext(arrowFn)
151
+ addFunction(
152
+ extractFunctionData(arrowFn, filePath, 'arrow', parentContext, false),
153
+ )
154
+ } else if (node.getKind() === SyntaxKind.FunctionExpression) {
155
+ const funcExpr = node as FunctionExpression
156
+ const parentContext = inferParentContext(funcExpr)
157
+ addFunction(
158
+ extractFunctionData(
159
+ funcExpr,
160
+ filePath,
161
+ 'function-expression',
162
+ parentContext,
163
+ false,
164
+ ),
165
+ )
166
+ }
167
+ })
168
+
169
+ return functions
170
+ }
171
+
172
+ /**
173
+ * Infer a meaningful name/context for an arrow function or function expression
174
+ * based on its position in the AST (e.g., method name it's passed to, property name, etc.)
175
+ */
176
+ function inferParentContext(
177
+ node: ArrowFunction | FunctionExpression,
178
+ ): string | null {
179
+ const parent = node.getParent()
180
+ if (!parent) return null
181
+
182
+ // Case 1: Passed as argument to a method call like .mutation(async () => {})
183
+ // Look for patterns like: .methodName(fn) or methodName(fn)
184
+ if (parent.getKind() === SyntaxKind.CallExpression) {
185
+ const callExpr = parent as CallExpression
186
+ const expression = callExpr.getExpression()
187
+
188
+ // Handle property access: obj.method(fn) -> "method"
189
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
190
+ const propAccess = expression as PropertyAccessExpression
191
+ return propAccess.getName()
192
+ }
193
+
194
+ // Handle direct call: method(fn) -> "method"
195
+ if (expression.getKind() === SyntaxKind.Identifier) {
196
+ return expression.getText()
197
+ }
198
+ }
199
+
200
+ // Case 2: Property assignment like { onClick: () => {} }
201
+ if (parent.getKind() === SyntaxKind.PropertyAssignment) {
202
+ const propAssignment = parent as import('ts-morph').PropertyAssignment
203
+ return propAssignment.getName()
204
+ }
205
+
206
+ // Case 3: Short-hand property like { onClick }
207
+ if (parent.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
208
+ const shorthand = parent as import('ts-morph').ShorthandPropertyAssignment
209
+ return shorthand.getName()
210
+ }
211
+
212
+ return null
213
+ }
214
+
215
+ /**
216
+ * Extract data from a function-like node
217
+ */
218
+ function extractFunctionData(
219
+ node: FunctionLikeNode,
220
+ filePath: string,
221
+ kind: ExtractedFunction['kind'],
222
+ parentContext: string | null = null,
223
+ isExportedOverride?: boolean,
224
+ ): ExtractedFunction {
225
+ const startLine = node.getStartLineNumber()
226
+ const endLine = node.getEndLineNumber()
227
+ const bodyLineCount = endLine - startLine + 1
228
+
229
+ // Get function name
230
+ let name: string | null = null
231
+ if ('getName' in node && typeof node.getName === 'function') {
232
+ name = node.getName() ?? null
233
+ }
234
+
235
+ // For arrow functions and function expressions assigned to variables,
236
+ // use the variable name as the function name if no intrinsic name exists
237
+ if (name === null && parentContext !== null) {
238
+ name = parentContext
239
+ }
240
+
241
+ // Check if async
242
+ const isAsync = 'isAsync' in node ? (node.isAsync?.() ?? false) : false
243
+
244
+ // Check if exported
245
+ let isExported = isExportedOverride ?? false
246
+ if (!isExported && 'isExported' in node) {
247
+ isExported = (node as FunctionDeclaration).isExported?.() ?? false
248
+ }
249
+
250
+ // Get body for analysis
251
+ const body = getBodyNode(node)
252
+
253
+ // Count statements
254
+ const statementCount = countStatements(body)
255
+
256
+ // Check for conditionals
257
+ const hasConditionals = checkForConditionals(body)
258
+
259
+ // Extract call sites
260
+ const callSites = extractCallSites(node, startLine)
261
+
262
+ // Check if any call is awaited
263
+ const hasAwait = callSites.some(cs => cs.isAwaited) || checkForAwait(node)
264
+
265
+ // Extract property access chains
266
+ const propertyAccessChains = extractPropertyAccessChains(node)
267
+
268
+ return {
269
+ name,
270
+ filePath,
271
+ startLine,
272
+ endLine,
273
+ isAsync,
274
+ isExported,
275
+ bodyLineCount,
276
+ statementCount,
277
+ hasConditionals,
278
+ parentContext,
279
+ callSites,
280
+ hasAwait,
281
+ propertyAccessChains,
282
+ kind,
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Get the body node of a function-like node
288
+ */
289
+ function getBodyNode(node: FunctionLikeNode): Node | undefined {
290
+ if ('getBody' in node) {
291
+ return node.getBody()
292
+ }
293
+ return undefined
294
+ }
295
+
296
+ /**
297
+ * Count statements in a function body
298
+ */
299
+ function countStatements(body: Node | undefined): number {
300
+ if (!body) return 0
301
+
302
+ // If it's a block, count statements
303
+ if (body.getKind() === SyntaxKind.Block) {
304
+ const statements = body.getChildrenOfKind(SyntaxKind.SyntaxList)
305
+ if (statements.length > 0 && statements[0]) {
306
+ return statements[0].getChildren().filter(isStatement).length
307
+ }
308
+ }
309
+
310
+ // If it's an expression body (arrow function), count as 1
311
+ return 1
312
+ }
313
+
314
+ /**
315
+ * Check if a node is a statement
316
+ */
317
+ function isStatement(node: Node): boolean {
318
+ const kind = node.getKind()
319
+ return (
320
+ kind === SyntaxKind.VariableStatement ||
321
+ kind === SyntaxKind.ExpressionStatement ||
322
+ kind === SyntaxKind.ReturnStatement ||
323
+ kind === SyntaxKind.IfStatement ||
324
+ kind === SyntaxKind.ForStatement ||
325
+ kind === SyntaxKind.ForInStatement ||
326
+ kind === SyntaxKind.ForOfStatement ||
327
+ kind === SyntaxKind.WhileStatement ||
328
+ kind === SyntaxKind.DoStatement ||
329
+ kind === SyntaxKind.SwitchStatement ||
330
+ kind === SyntaxKind.TryStatement ||
331
+ kind === SyntaxKind.ThrowStatement ||
332
+ kind === SyntaxKind.BreakStatement ||
333
+ kind === SyntaxKind.ContinueStatement
334
+ )
335
+ }
336
+
337
+ /**
338
+ * Check if a function body contains conditionals
339
+ */
340
+ function checkForConditionals(body: Node | undefined): boolean {
341
+ if (!body) return false
342
+
343
+ let hasConditionals = false
344
+
345
+ body.forEachDescendant(node => {
346
+ const kind = node.getKind()
347
+ if (
348
+ kind === SyntaxKind.IfStatement ||
349
+ kind === SyntaxKind.ConditionalExpression ||
350
+ kind === SyntaxKind.SwitchStatement
351
+ ) {
352
+ hasConditionals = true
353
+ }
354
+ })
355
+
356
+ return hasConditionals
357
+ }
358
+
359
+ /**
360
+ * Check if a function contains await expressions
361
+ */
362
+ function checkForAwait(node: FunctionLikeNode): boolean {
363
+ let hasAwait = false
364
+
365
+ node.forEachDescendant((descendant, traversal) => {
366
+ // Don't descend into nested functions
367
+ const kind = descendant.getKind()
368
+ if (
369
+ kind === SyntaxKind.FunctionDeclaration ||
370
+ kind === SyntaxKind.FunctionExpression ||
371
+ kind === SyntaxKind.ArrowFunction ||
372
+ kind === SyntaxKind.MethodDeclaration
373
+ ) {
374
+ traversal.skip()
375
+ return
376
+ }
377
+
378
+ if (kind === SyntaxKind.AwaitExpression) {
379
+ hasAwait = true
380
+ }
381
+ })
382
+
383
+ return hasAwait
384
+ }
385
+
386
+ /**
387
+ * Extract call sites from a function
388
+ */
389
+ function extractCallSites(
390
+ node: FunctionLikeNode,
391
+ functionStartLine: number,
392
+ ): CallSite[] {
393
+ const callSites: CallSite[] = []
394
+
395
+ node.forEachDescendant((descendant, traversal) => {
396
+ // Don't descend into nested functions
397
+ const kind = descendant.getKind()
398
+ if (
399
+ kind === SyntaxKind.FunctionDeclaration ||
400
+ kind === SyntaxKind.FunctionExpression ||
401
+ kind === SyntaxKind.ArrowFunction ||
402
+ kind === SyntaxKind.MethodDeclaration
403
+ ) {
404
+ traversal.skip()
405
+ return
406
+ }
407
+
408
+ if (kind === SyntaxKind.CallExpression) {
409
+ const callExpr = descendant as CallExpression
410
+ const expression = getCallExpressionText(callExpr)
411
+ const line = callExpr.getStartLineNumber() - functionStartLine
412
+
413
+ // Check if this call is awaited
414
+ const parent = callExpr.getParent()
415
+ const isAwaited = parent?.getKind() === SyntaxKind.AwaitExpression
416
+
417
+ callSites.push({
418
+ expression,
419
+ line,
420
+ isAwaited,
421
+ })
422
+ }
423
+ })
424
+
425
+ return callSites
426
+ }
427
+
428
+ /**
429
+ * Get the text representation of a call expression
430
+ */
431
+ function getCallExpressionText(callExpr: CallExpression): string {
432
+ const expression = callExpr.getExpression()
433
+
434
+ // Handle property access chains like db.user.findFirst
435
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
436
+ return getPropertyAccessChainText(expression as PropertyAccessExpression)
437
+ }
438
+
439
+ // Handle simple function calls
440
+ return expression.getText()
441
+ }
442
+
443
+ /**
444
+ * Get the full text of a property access chain
445
+ */
446
+ function getPropertyAccessChainText(node: PropertyAccessExpression): string {
447
+ const parts: string[] = []
448
+ let current: Node = node
449
+
450
+ while (current.getKind() === SyntaxKind.PropertyAccessExpression) {
451
+ const propAccess = current as PropertyAccessExpression
452
+ parts.unshift(propAccess.getName())
453
+ current = propAccess.getExpression()
454
+ }
455
+
456
+ // Add the base expression
457
+ parts.unshift(current.getText())
458
+
459
+ return parts.join('.')
460
+ }
461
+
462
+ /**
463
+ * Extract property access chains from a function
464
+ */
465
+ function extractPropertyAccessChains(node: FunctionLikeNode): string[] {
466
+ const chains = new Set<string>()
467
+
468
+ node.forEachDescendant((descendant, traversal) => {
469
+ // Don't descend into nested functions
470
+ const kind = descendant.getKind()
471
+ if (
472
+ kind === SyntaxKind.FunctionDeclaration ||
473
+ kind === SyntaxKind.FunctionExpression ||
474
+ kind === SyntaxKind.ArrowFunction ||
475
+ kind === SyntaxKind.MethodDeclaration
476
+ ) {
477
+ traversal.skip()
478
+ return
479
+ }
480
+
481
+ if (kind === SyntaxKind.PropertyAccessExpression) {
482
+ const propAccess = descendant as PropertyAccessExpression
483
+ // Only get top-level property access chains (not nested ones)
484
+ const parent = propAccess.getParent()
485
+ if (parent?.getKind() !== SyntaxKind.PropertyAccessExpression) {
486
+ const chain = getPropertyAccessChainText(propAccess)
487
+ // Filter out simple single-level access
488
+ if (chain.includes('.')) {
489
+ chains.add(chain)
490
+ }
491
+ }
492
+ }
493
+ })
494
+
495
+ return Array.from(chains)
496
+ }
497
+
498
+ /**
499
+ * Extract imports from a source file
500
+ */
501
+ export function extractImports(sourceFile: SourceFile): FileImports {
502
+ const filePath = sourceFile.getFilePath()
503
+ const imports: FileImports['imports'] = []
504
+
505
+ for (const importDecl of sourceFile.getImportDeclarations()) {
506
+ const moduleSpecifier = importDecl.getModuleSpecifierValue()
507
+ const namedImports: string[] = []
508
+
509
+ // Get named imports
510
+ for (const namedImport of importDecl.getNamedImports()) {
511
+ namedImports.push(namedImport.getName())
512
+ }
513
+
514
+ // Get default import
515
+ const defaultImport = importDecl.getDefaultImport()
516
+ if (defaultImport) {
517
+ namedImports.push(defaultImport.getText())
518
+ }
519
+
520
+ // Get namespace import
521
+ const namespaceImport = importDecl.getNamespaceImport()
522
+ if (namespaceImport) {
523
+ namedImports.push(namespaceImport.getText())
524
+ }
525
+
526
+ imports.push({
527
+ moduleSpecifier,
528
+ namedImports,
529
+ })
530
+ }
531
+
532
+ return {
533
+ filePath,
534
+ imports,
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Check if a file is type-only (contains only types, interfaces, enums, no function bodies)
540
+ */
541
+ export function isTypeOnlyFile(sourceFile: SourceFile): boolean {
542
+ // Check for function declarations
543
+ if (sourceFile.getFunctions().length > 0) return false
544
+
545
+ // Check for classes with methods
546
+ for (const classDecl of sourceFile.getClasses()) {
547
+ if (classDecl.getMethods().length > 0) return false
548
+ if (classDecl.getGetAccessors().length > 0) return false
549
+ if (classDecl.getSetAccessors().length > 0) return false
550
+ }
551
+
552
+ // Check for variable declarations with functions
553
+ for (const varStatement of sourceFile.getVariableStatements()) {
554
+ for (const decl of varStatement.getDeclarations()) {
555
+ const initializer = decl.getInitializer()
556
+ if (initializer) {
557
+ const kind = initializer.getKind()
558
+ if (
559
+ kind === SyntaxKind.ArrowFunction ||
560
+ kind === SyntaxKind.FunctionExpression
561
+ ) {
562
+ return false
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ // If we got here, file has no function bodies
569
+ return true
570
+ }