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.
- package/.plans/001-fcis-analyzer.md +832 -0
- package/.plans/002-fcis-analyzer-improvements.md +205 -0
- package/README.md +272 -0
- package/TECHNICAL.md +386 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1836 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +709 -0
- package/dist/index.js +1845 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/pnpm-workspace.yaml +0 -0
- package/src/analyzer.ts +266 -0
- package/src/classification/classifier.ts +156 -0
- package/src/classification/derive-status.ts +171 -0
- package/src/classification/quality-scorer.ts +481 -0
- package/src/cli.ts +286 -0
- package/src/detection/detect-markers.ts +480 -0
- package/src/detection/markers.ts +332 -0
- package/src/extraction/extract-functions.ts +570 -0
- package/src/extraction/extractor.ts +188 -0
- package/src/index.ts +111 -0
- package/src/reporting/report-console.ts +416 -0
- package/src/reporting/report-json.ts +232 -0
- package/src/scoring/scorer.ts +504 -0
- package/src/types.ts +248 -0
- package/tests/classifier.test.ts +480 -0
- package/tests/derive-status.test.ts +464 -0
- package/tests/detect-markers.test.ts +639 -0
- package/tests/extractor.test.ts +155 -0
- package/tests/integration.test.ts +706 -0
- package/tests/quality-scorer.test.ts +650 -0
- package/tests/scorer.test.ts +768 -0
- package/tsconfig.json +34 -0
- package/tsup.config.ts +17 -0
- package/vendor/ts-morph/.editorconfig +10 -0
- package/vendor/ts-morph/.gitattributes +11 -0
- package/vendor/ts-morph/.github/CODE_OF_CONDUCT.md +77 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/custom.md +4 -0
- package/vendor/ts-morph/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
- package/vendor/ts-morph/.github/workflows/ci.yml +50 -0
- package/vendor/ts-morph/.github/workflows/publish.yml +53 -0
- package/vendor/ts-morph/.vscode/settings.json +10 -0
- package/vendor/ts-morph/CONTRIBUTING.md +23 -0
- package/vendor/ts-morph/DEVELOPMENT.md +32 -0
- package/vendor/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/deno.json +8 -0
- package/vendor/ts-morph/deno.lock +1233 -0
- package/vendor/ts-morph/docs/CNAME +1 -0
- package/vendor/ts-morph/docs/Gemfile +2 -0
- package/vendor/ts-morph/docs/_config.yml +5 -0
- package/vendor/ts-morph/docs/_layouts/default.html +159 -0
- package/vendor/ts-morph/docs/_script-templates/main.ts +116 -0
- package/vendor/ts-morph/docs/assets/css/style.scss +212 -0
- package/vendor/ts-morph/docs/details/ambient.md +38 -0
- package/vendor/ts-morph/docs/details/async.md +31 -0
- package/vendor/ts-morph/docs/details/classes.md +314 -0
- package/vendor/ts-morph/docs/details/comment-ranges.md +7 -0
- package/vendor/ts-morph/docs/details/comments.md +122 -0
- package/vendor/ts-morph/docs/details/decorators.md +119 -0
- package/vendor/ts-morph/docs/details/documentation.md +73 -0
- package/vendor/ts-morph/docs/details/enums.md +117 -0
- package/vendor/ts-morph/docs/details/exports.md +308 -0
- package/vendor/ts-morph/docs/details/expressions.md +46 -0
- package/vendor/ts-morph/docs/details/functions.md +150 -0
- package/vendor/ts-morph/docs/details/generators.md +27 -0
- package/vendor/ts-morph/docs/details/identifiers.md +79 -0
- package/vendor/ts-morph/docs/details/imports.md +191 -0
- package/vendor/ts-morph/docs/details/index.md +52 -0
- package/vendor/ts-morph/docs/details/initializers.md +40 -0
- package/vendor/ts-morph/docs/details/interfaces.md +218 -0
- package/vendor/ts-morph/docs/details/literals.md +20 -0
- package/vendor/ts-morph/docs/details/modifiers.md +38 -0
- package/vendor/ts-morph/docs/details/modules.md +113 -0
- package/vendor/ts-morph/docs/details/namespaces.md +7 -0
- package/vendor/ts-morph/docs/details/object-literal-expressions.md +106 -0
- package/vendor/ts-morph/docs/details/parameters.md +64 -0
- package/vendor/ts-morph/docs/details/signatures.md +41 -0
- package/vendor/ts-morph/docs/details/source-files.md +292 -0
- package/vendor/ts-morph/docs/details/type-aliases.md +34 -0
- package/vendor/ts-morph/docs/details/type-parameters.md +72 -0
- package/vendor/ts-morph/docs/details/types.md +254 -0
- package/vendor/ts-morph/docs/details/variables.md +110 -0
- package/vendor/ts-morph/docs/emitting.md +151 -0
- package/vendor/ts-morph/docs/index.md +25 -0
- package/vendor/ts-morph/docs/manipulation/code-writer.md +20 -0
- package/vendor/ts-morph/docs/manipulation/formatting.md +76 -0
- package/vendor/ts-morph/docs/manipulation/index.md +136 -0
- package/vendor/ts-morph/docs/manipulation/order.md +14 -0
- package/vendor/ts-morph/docs/manipulation/performance.md +222 -0
- package/vendor/ts-morph/docs/manipulation/removing.md +31 -0
- package/vendor/ts-morph/docs/manipulation/renaming.md +106 -0
- package/vendor/ts-morph/docs/manipulation/settings.md +76 -0
- package/vendor/ts-morph/docs/manipulation/structures.md +117 -0
- package/vendor/ts-morph/docs/manipulation/transforms.md +84 -0
- package/vendor/ts-morph/docs/metrics/performance.json +4 -0
- package/vendor/ts-morph/docs/navigation/ambient-modules.md +22 -0
- package/vendor/ts-morph/docs/navigation/compiler-nodes.md +82 -0
- package/vendor/ts-morph/docs/navigation/directories.md +287 -0
- package/vendor/ts-morph/docs/navigation/example.md +50 -0
- package/vendor/ts-morph/docs/navigation/finding-references.md +53 -0
- package/vendor/ts-morph/docs/navigation/getting-source-files.md +59 -0
- package/vendor/ts-morph/docs/navigation/images/getChildrenVsForEachChild.gif +0 -0
- package/vendor/ts-morph/docs/navigation/index.md +94 -0
- package/vendor/ts-morph/docs/navigation/language-service.md +23 -0
- package/vendor/ts-morph/docs/navigation/program.md +25 -0
- package/vendor/ts-morph/docs/navigation/type-checker.md +33 -0
- package/vendor/ts-morph/docs/setup/adding-source-files.md +145 -0
- package/vendor/ts-morph/docs/setup/ast-viewers.md +46 -0
- package/vendor/ts-morph/docs/setup/diagnostics.md +109 -0
- package/vendor/ts-morph/docs/setup/file-system.md +106 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-ast_small.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-command-palette.png +0 -0
- package/vendor/ts-morph/docs/setup/images/atom-file.png +0 -0
- package/vendor/ts-morph/docs/setup/images/ts-ast-viewer.png +0 -0
- package/vendor/ts-morph/docs/setup/index.md +94 -0
- package/vendor/ts-morph/docs/utilities.md +55 -0
- package/vendor/ts-morph/dprint.json +23 -0
- package/vendor/ts-morph/package.json +30 -0
- package/vendor/ts-morph/packages/bootstrap/LICENSE +21 -0
- package/vendor/ts-morph/packages/bootstrap/lib/ts-morph-bootstrap.d.ts +397 -0
- package/vendor/ts-morph/packages/bootstrap/package.json +46 -0
- package/vendor/ts-morph/packages/bootstrap/readme.md +200 -0
- package/vendor/ts-morph/packages/common/LICENSE +21 -0
- package/vendor/ts-morph/packages/common/lib/ts-morph-common.d.ts +1082 -0
- package/vendor/ts-morph/packages/common/lib/typescript.d.ts +11439 -0
- package/vendor/ts-morph/packages/common/package.json +65 -0
- package/vendor/ts-morph/packages/common/readme.md +5 -0
- package/vendor/ts-morph/packages/scripts/changeTypeScriptVersion.ts +28 -0
- package/vendor/ts-morph/packages/scripts/createDeclarationProject.ts +47 -0
- package/vendor/ts-morph/packages/scripts/deps.ts +2 -0
- package/vendor/ts-morph/packages/scripts/execScript.ts +31 -0
- package/vendor/ts-morph/packages/scripts/folders.ts +11 -0
- package/vendor/ts-morph/packages/scripts/getDevCompilerVersions.ts +19 -0
- package/vendor/ts-morph/packages/scripts/mod.ts +7 -0
- package/vendor/ts-morph/packages/scripts/utils/Memoize.ts +36 -0
- package/vendor/ts-morph/packages/scripts/utils/forEachTypeText.ts +23 -0
- package/vendor/ts-morph/packages/scripts/utils/makeConstructorsPrivate.ts +26 -0
- package/vendor/ts-morph/packages/scripts/utils/mod.ts +4 -0
- package/vendor/ts-morph/packages/scripts/utils/printDiagnostics.ts +10 -0
- package/vendor/ts-morph/packages/ts-morph/LICENSE +21 -0
- package/vendor/ts-morph/packages/ts-morph/lib/ts-morph.d.ts +11198 -0
- package/vendor/ts-morph/packages/ts-morph/package.json +78 -0
- package/vendor/ts-morph/packages/ts-morph/readme.md +111 -0
- package/vendor/ts-morph/readme.md +14 -0
- package/vendor/ts-morph/rfcs/README.md +13 -0
- package/vendor/ts-morph/rfcs/RFC-0001 - Inserting Into Statements Handling Comments.md +181 -0
- package/vendor/ts-morph/tsconfig.common.json +17 -0
- 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
|
+
}
|