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,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Scorer - Pure Core
|
|
3
|
+
*
|
|
4
|
+
* Computes quality scores (0-100) for impure functions measuring how
|
|
5
|
+
* well-structured the impurity is. This is a PURE module - all functions
|
|
6
|
+
* take data in and return data out with no I/O.
|
|
7
|
+
*
|
|
8
|
+
* Quality signals measure structural quality, not whether impurity exists.
|
|
9
|
+
* A high score means the impure function is well-organized and maintainable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
CallSite,
|
|
14
|
+
DetectionContext,
|
|
15
|
+
ExtractedFunction,
|
|
16
|
+
ImpurityMarker,
|
|
17
|
+
MarkerType,
|
|
18
|
+
} from '../types.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Quality scoring weights and thresholds
|
|
22
|
+
*/
|
|
23
|
+
export const QUALITY_WEIGHTS = {
|
|
24
|
+
// Positive signals (additive)
|
|
25
|
+
callsPureFile: 30, // Calls a function from .pure.ts file
|
|
26
|
+
callsPureNamingConvention: 20, // Calls plan*/derive*/compute*/calculate*/transform*
|
|
27
|
+
ioConcentratedAtStart: 15, // I/O calls concentrated at start (GATHER pattern)
|
|
28
|
+
ioConcentratedAtEnd: 15, // I/O calls concentrated at end (EXECUTE pattern)
|
|
29
|
+
lowComplexity: 10, // Low cyclomatic complexity (≤ 5)
|
|
30
|
+
shellNamingConvention: 5, // Function name matches shell conventions
|
|
31
|
+
callsPredicateFunctions: 5, // Calls is*/has*/should* functions
|
|
32
|
+
|
|
33
|
+
// Negative signals (penalties)
|
|
34
|
+
ioInterleaved: -20, // I/O calls interleaved throughout
|
|
35
|
+
highComplexity: -15, // High cyclomatic complexity (> 10)
|
|
36
|
+
multipleIoTypes: -10, // Multiple I/O types in same function
|
|
37
|
+
noPureFunctionCalls: -10, // No pure function calls at all
|
|
38
|
+
veryLongFunction: -10, // > 100 lines
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pure function naming patterns
|
|
43
|
+
*/
|
|
44
|
+
const PURE_FUNCTION_PATTERNS = [
|
|
45
|
+
/^plan[A-Z]/,
|
|
46
|
+
/^derive[A-Z]/,
|
|
47
|
+
/^compute[A-Z]/,
|
|
48
|
+
/^calculate[A-Z]/,
|
|
49
|
+
/^transform[A-Z]/,
|
|
50
|
+
/^build[A-Z]/,
|
|
51
|
+
/^create[A-Z](?!.*Service)/, // createX but not createXService
|
|
52
|
+
/^parse[A-Z]/,
|
|
53
|
+
/^format[A-Z]/,
|
|
54
|
+
/^validate[A-Z]/,
|
|
55
|
+
/^normalize[A-Z]/,
|
|
56
|
+
/^convert[A-Z]/,
|
|
57
|
+
/^map[A-Z]/,
|
|
58
|
+
/^filter[A-Z]/,
|
|
59
|
+
/^reduce[A-Z]/,
|
|
60
|
+
/^merge[A-Z]/,
|
|
61
|
+
/^extract[A-Z]/,
|
|
62
|
+
/^to[A-Z]/, // toPublicX, toDTO, etc.
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Predicate function patterns (return boolean)
|
|
67
|
+
*/
|
|
68
|
+
const PREDICATE_PATTERNS = [
|
|
69
|
+
/^is[A-Z]/,
|
|
70
|
+
/^has[A-Z]/,
|
|
71
|
+
/^should[A-Z]/,
|
|
72
|
+
/^can[A-Z]/,
|
|
73
|
+
/^will[A-Z]/,
|
|
74
|
+
/^was[A-Z]/,
|
|
75
|
+
/^did[A-Z]/,
|
|
76
|
+
/^are[A-Z]/,
|
|
77
|
+
/^does[A-Z]/,
|
|
78
|
+
/^needs[A-Z]/,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Shell function naming patterns
|
|
83
|
+
*/
|
|
84
|
+
const SHELL_FUNCTION_PATTERNS = [
|
|
85
|
+
/^handle[A-Z]/,
|
|
86
|
+
/^fetch[A-Z]/,
|
|
87
|
+
/^save[A-Z]/,
|
|
88
|
+
/^send[A-Z]/,
|
|
89
|
+
/^load[A-Z]/,
|
|
90
|
+
/^get[A-Z]/, // getData, getUser, etc.
|
|
91
|
+
/^set[A-Z]/,
|
|
92
|
+
/^update[A-Z]/,
|
|
93
|
+
/^delete[A-Z]/,
|
|
94
|
+
/^remove[A-Z]/,
|
|
95
|
+
/^create[A-Z].*Service/, // createXService
|
|
96
|
+
/^init[A-Z]/,
|
|
97
|
+
/^process[A-Z]/,
|
|
98
|
+
/^execute[A-Z]/,
|
|
99
|
+
/^run[A-Z]/,
|
|
100
|
+
/^perform[A-Z]/,
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* I/O marker types for concentration analysis
|
|
105
|
+
*/
|
|
106
|
+
const IO_MARKER_TYPES: Set<MarkerType> = new Set([
|
|
107
|
+
'await-expression',
|
|
108
|
+
'database-call',
|
|
109
|
+
'network-fetch',
|
|
110
|
+
'network-http',
|
|
111
|
+
'fs-call',
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute quality score for an impure function
|
|
116
|
+
*
|
|
117
|
+
* @param fn - The extracted function data
|
|
118
|
+
* @param markers - The detected impurity markers
|
|
119
|
+
* @param context - Detection context with import information
|
|
120
|
+
* @returns Quality score from 0-100
|
|
121
|
+
*/
|
|
122
|
+
export function computeQualityScore(
|
|
123
|
+
fn: ExtractedFunction,
|
|
124
|
+
markers: ImpurityMarker[],
|
|
125
|
+
context: DetectionContext,
|
|
126
|
+
): number {
|
|
127
|
+
let score = 50 // Start at neutral
|
|
128
|
+
|
|
129
|
+
// Analyze the function and accumulate score adjustments
|
|
130
|
+
const analysis = analyzeFunction(fn, markers, context)
|
|
131
|
+
|
|
132
|
+
// Apply positive signals
|
|
133
|
+
if (analysis.callsPureFile) {
|
|
134
|
+
score += QUALITY_WEIGHTS.callsPureFile
|
|
135
|
+
}
|
|
136
|
+
if (analysis.callsPureNamingConvention) {
|
|
137
|
+
score += QUALITY_WEIGHTS.callsPureNamingConvention
|
|
138
|
+
}
|
|
139
|
+
if (analysis.ioConcentratedAtStart) {
|
|
140
|
+
score += QUALITY_WEIGHTS.ioConcentratedAtStart
|
|
141
|
+
}
|
|
142
|
+
if (analysis.ioConcentratedAtEnd) {
|
|
143
|
+
score += QUALITY_WEIGHTS.ioConcentratedAtEnd
|
|
144
|
+
}
|
|
145
|
+
if (analysis.lowComplexity) {
|
|
146
|
+
score += QUALITY_WEIGHTS.lowComplexity
|
|
147
|
+
}
|
|
148
|
+
if (analysis.shellNamingConvention) {
|
|
149
|
+
score += QUALITY_WEIGHTS.shellNamingConvention
|
|
150
|
+
}
|
|
151
|
+
if (analysis.callsPredicateFunctions) {
|
|
152
|
+
score += QUALITY_WEIGHTS.callsPredicateFunctions
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Apply negative signals (penalties)
|
|
156
|
+
if (analysis.ioInterleaved) {
|
|
157
|
+
score += QUALITY_WEIGHTS.ioInterleaved
|
|
158
|
+
}
|
|
159
|
+
if (analysis.highComplexity) {
|
|
160
|
+
score += QUALITY_WEIGHTS.highComplexity
|
|
161
|
+
}
|
|
162
|
+
if (analysis.multipleIoTypes) {
|
|
163
|
+
score += QUALITY_WEIGHTS.multipleIoTypes
|
|
164
|
+
}
|
|
165
|
+
if (analysis.noPureFunctionCalls) {
|
|
166
|
+
score += QUALITY_WEIGHTS.noPureFunctionCalls
|
|
167
|
+
}
|
|
168
|
+
if (analysis.veryLongFunction) {
|
|
169
|
+
score += QUALITY_WEIGHTS.veryLongFunction
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Clamp score to 0-100
|
|
173
|
+
return Math.max(0, Math.min(100, score))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analysis result for a function
|
|
178
|
+
*/
|
|
179
|
+
export type FunctionAnalysis = {
|
|
180
|
+
// Positive signals
|
|
181
|
+
callsPureFile: boolean
|
|
182
|
+
callsPureNamingConvention: boolean
|
|
183
|
+
ioConcentratedAtStart: boolean
|
|
184
|
+
ioConcentratedAtEnd: boolean
|
|
185
|
+
lowComplexity: boolean
|
|
186
|
+
shellNamingConvention: boolean
|
|
187
|
+
callsPredicateFunctions: boolean
|
|
188
|
+
|
|
189
|
+
// Negative signals
|
|
190
|
+
ioInterleaved: boolean
|
|
191
|
+
highComplexity: boolean
|
|
192
|
+
multipleIoTypes: boolean
|
|
193
|
+
noPureFunctionCalls: boolean
|
|
194
|
+
veryLongFunction: boolean
|
|
195
|
+
|
|
196
|
+
// Metrics
|
|
197
|
+
estimatedComplexity: number
|
|
198
|
+
ioMarkerCount: number
|
|
199
|
+
uniqueIoTypes: Set<MarkerType>
|
|
200
|
+
pureCallCount: number
|
|
201
|
+
predicateCallCount: number
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Analyze a function for quality signals
|
|
206
|
+
*/
|
|
207
|
+
export function analyzeFunction(
|
|
208
|
+
fn: ExtractedFunction,
|
|
209
|
+
markers: ImpurityMarker[],
|
|
210
|
+
context: DetectionContext,
|
|
211
|
+
): FunctionAnalysis {
|
|
212
|
+
// Get I/O markers with line information
|
|
213
|
+
const ioMarkers = markers.filter(
|
|
214
|
+
m => IO_MARKER_TYPES.has(m.type) && m.line !== undefined,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Unique I/O types
|
|
218
|
+
const uniqueIoTypes = new Set(markers.map(m => m.type))
|
|
219
|
+
|
|
220
|
+
// Check for pure file imports being called
|
|
221
|
+
const callsPureFile = checkCallsPureFile(fn, context)
|
|
222
|
+
|
|
223
|
+
// Check for pure naming convention calls
|
|
224
|
+
const { callsPureNaming, pureCallCount } = checkCallsPureNamingConvention(
|
|
225
|
+
fn.callSites,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// Check for predicate function calls
|
|
229
|
+
const { callsPredicates, predicateCallCount } = checkCallsPredicateFunctions(
|
|
230
|
+
fn.callSites,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// Check I/O concentration
|
|
234
|
+
const { concentratedAtStart, concentratedAtEnd, interleaved } =
|
|
235
|
+
analyzeIoConcentration(fn, ioMarkers)
|
|
236
|
+
|
|
237
|
+
// Estimate complexity
|
|
238
|
+
const estimatedComplexity = estimateCyclomaticComplexity(fn)
|
|
239
|
+
|
|
240
|
+
// Check shell naming convention
|
|
241
|
+
const shellNamingConvention = checkShellNamingConvention(fn.name)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
callsPureFile,
|
|
245
|
+
callsPureNamingConvention: callsPureNaming,
|
|
246
|
+
ioConcentratedAtStart: concentratedAtStart,
|
|
247
|
+
ioConcentratedAtEnd: concentratedAtEnd,
|
|
248
|
+
lowComplexity: estimatedComplexity <= 5,
|
|
249
|
+
shellNamingConvention,
|
|
250
|
+
callsPredicateFunctions: callsPredicates,
|
|
251
|
+
|
|
252
|
+
ioInterleaved: interleaved,
|
|
253
|
+
highComplexity: estimatedComplexity > 10,
|
|
254
|
+
multipleIoTypes: uniqueIoTypes.size >= 3,
|
|
255
|
+
noPureFunctionCalls: pureCallCount === 0 && predicateCallCount === 0,
|
|
256
|
+
veryLongFunction: fn.bodyLineCount > 100,
|
|
257
|
+
|
|
258
|
+
estimatedComplexity,
|
|
259
|
+
ioMarkerCount: ioMarkers.length,
|
|
260
|
+
uniqueIoTypes,
|
|
261
|
+
pureCallCount,
|
|
262
|
+
predicateCallCount,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if function calls any imports from .pure files
|
|
268
|
+
*/
|
|
269
|
+
function checkCallsPureFile(
|
|
270
|
+
fn: ExtractedFunction,
|
|
271
|
+
context: DetectionContext,
|
|
272
|
+
): boolean {
|
|
273
|
+
// Get names of functions imported from .pure files
|
|
274
|
+
const pureImports = new Set<string>()
|
|
275
|
+
for (const imp of context.imports.imports) {
|
|
276
|
+
if (context.pureFileImports.has(imp.moduleSpecifier)) {
|
|
277
|
+
for (const name of imp.namedImports) {
|
|
278
|
+
pureImports.add(name)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if any call site calls a pure import
|
|
284
|
+
for (const callSite of fn.callSites) {
|
|
285
|
+
const calledFn = extractFunctionName(callSite.expression)
|
|
286
|
+
if (pureImports.has(calledFn)) {
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if function calls functions with pure naming conventions
|
|
296
|
+
*/
|
|
297
|
+
function checkCallsPureNamingConvention(callSites: CallSite[]): {
|
|
298
|
+
callsPureNaming: boolean
|
|
299
|
+
pureCallCount: number
|
|
300
|
+
} {
|
|
301
|
+
let pureCallCount = 0
|
|
302
|
+
|
|
303
|
+
for (const callSite of callSites) {
|
|
304
|
+
const calledFn = extractFunctionName(callSite.expression)
|
|
305
|
+
if (matchesAnyPattern(calledFn, PURE_FUNCTION_PATTERNS)) {
|
|
306
|
+
pureCallCount++
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
callsPureNaming: pureCallCount > 0,
|
|
312
|
+
pureCallCount,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if function calls predicate functions
|
|
318
|
+
*/
|
|
319
|
+
function checkCallsPredicateFunctions(callSites: CallSite[]): {
|
|
320
|
+
callsPredicates: boolean
|
|
321
|
+
predicateCallCount: number
|
|
322
|
+
} {
|
|
323
|
+
let predicateCallCount = 0
|
|
324
|
+
|
|
325
|
+
for (const callSite of callSites) {
|
|
326
|
+
const calledFn = extractFunctionName(callSite.expression)
|
|
327
|
+
if (matchesAnyPattern(calledFn, PREDICATE_PATTERNS)) {
|
|
328
|
+
predicateCallCount++
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
callsPredicates: predicateCallCount > 0,
|
|
334
|
+
predicateCallCount,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Analyze I/O concentration within a function
|
|
340
|
+
*
|
|
341
|
+
* Good FCIS patterns:
|
|
342
|
+
* - GATHER: I/O at the start, then pure logic
|
|
343
|
+
* - EXECUTE: Pure logic, then I/O at the end
|
|
344
|
+
* - GATHER-DECIDE-EXECUTE: I/O at start, pure in middle, I/O at end
|
|
345
|
+
*
|
|
346
|
+
* Bad pattern:
|
|
347
|
+
* - Interleaved: I/O calls scattered throughout
|
|
348
|
+
*/
|
|
349
|
+
function analyzeIoConcentration(
|
|
350
|
+
fn: ExtractedFunction,
|
|
351
|
+
ioMarkers: ImpurityMarker[],
|
|
352
|
+
): {
|
|
353
|
+
concentratedAtStart: boolean
|
|
354
|
+
concentratedAtEnd: boolean
|
|
355
|
+
interleaved: boolean
|
|
356
|
+
} {
|
|
357
|
+
if (ioMarkers.length === 0) {
|
|
358
|
+
return {
|
|
359
|
+
concentratedAtStart: false,
|
|
360
|
+
concentratedAtEnd: false,
|
|
361
|
+
interleaved: false,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Get I/O positions as percentages through the function
|
|
366
|
+
const positions = ioMarkers
|
|
367
|
+
.map(m => (m.line ?? 0) / Math.max(fn.bodyLineCount, 1))
|
|
368
|
+
.sort((a, b) => a - b)
|
|
369
|
+
|
|
370
|
+
// Calculate concentration metrics
|
|
371
|
+
const startThreshold = 0.33 // First third
|
|
372
|
+
const endThreshold = 0.67 // Last third
|
|
373
|
+
|
|
374
|
+
const atStart = positions.filter(p => p <= startThreshold).length
|
|
375
|
+
const atEnd = positions.filter(p => p >= endThreshold).length
|
|
376
|
+
const inMiddle = positions.filter(
|
|
377
|
+
p => p > startThreshold && p < endThreshold,
|
|
378
|
+
).length
|
|
379
|
+
|
|
380
|
+
const total = positions.length
|
|
381
|
+
|
|
382
|
+
// Concentrated at start: most I/O in first third
|
|
383
|
+
const concentratedAtStart = atStart >= total * 0.6 && inMiddle <= 1
|
|
384
|
+
|
|
385
|
+
// Concentrated at end: most I/O in last third
|
|
386
|
+
const concentratedAtEnd = atEnd >= total * 0.6 && inMiddle <= 1
|
|
387
|
+
|
|
388
|
+
// Interleaved: I/O scattered throughout
|
|
389
|
+
// Consider it interleaved if there's significant I/O in all sections
|
|
390
|
+
// or if there are many transitions between I/O and non-I/O regions
|
|
391
|
+
const interleaved =
|
|
392
|
+
inMiddle >= 2 ||
|
|
393
|
+
(atStart > 0 && atEnd > 0 && inMiddle > 0) ||
|
|
394
|
+
(total >= 4 && !concentratedAtStart && !concentratedAtEnd)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
concentratedAtStart,
|
|
398
|
+
concentratedAtEnd,
|
|
399
|
+
interleaved,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Estimate cyclomatic complexity from extracted function data
|
|
405
|
+
*
|
|
406
|
+
* Cyclomatic complexity = 1 + number of decision points
|
|
407
|
+
* Since we don't have full AST access here, we estimate based on:
|
|
408
|
+
* - hasConditionals flag
|
|
409
|
+
* - statement count
|
|
410
|
+
* - body line count
|
|
411
|
+
*/
|
|
412
|
+
function estimateCyclomaticComplexity(fn: ExtractedFunction): number {
|
|
413
|
+
let complexity = 1 // Base complexity
|
|
414
|
+
|
|
415
|
+
// If has conditionals, estimate based on statement density
|
|
416
|
+
if (fn.hasConditionals) {
|
|
417
|
+
// Rough estimate: conditionals add 1-2 per ~10 statements
|
|
418
|
+
complexity += Math.ceil(fn.statementCount / 10)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Factor in line count for very long functions
|
|
422
|
+
if (fn.bodyLineCount > 50) {
|
|
423
|
+
complexity += Math.floor((fn.bodyLineCount - 50) / 25)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return complexity
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Check if function name matches shell naming conventions
|
|
431
|
+
*/
|
|
432
|
+
function checkShellNamingConvention(name: string | null): boolean {
|
|
433
|
+
if (!name) return false
|
|
434
|
+
return matchesAnyPattern(name, SHELL_FUNCTION_PATTERNS)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Extract the function name from a call expression
|
|
439
|
+
* e.g., "db.user.findFirst" -> "findFirst"
|
|
440
|
+
* "planAcceptInvite" -> "planAcceptInvite"
|
|
441
|
+
* "this.validate" -> "validate"
|
|
442
|
+
*/
|
|
443
|
+
function extractFunctionName(expression: string): string {
|
|
444
|
+
const parts = expression.split('.')
|
|
445
|
+
return parts[parts.length - 1] ?? expression
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Check if a string matches any of the given patterns
|
|
450
|
+
*/
|
|
451
|
+
function matchesAnyPattern(str: string, patterns: RegExp[]): boolean {
|
|
452
|
+
return patterns.some(pattern => pattern.test(str))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get quality score interpretation
|
|
457
|
+
*/
|
|
458
|
+
export function interpretQualityScore(score: number): {
|
|
459
|
+
level: 'high' | 'medium' | 'low'
|
|
460
|
+
description: string
|
|
461
|
+
} {
|
|
462
|
+
if (score >= 70) {
|
|
463
|
+
return {
|
|
464
|
+
level: 'high',
|
|
465
|
+
description:
|
|
466
|
+
'Well-structured impure function. I/O is organized and pure logic is separated.',
|
|
467
|
+
}
|
|
468
|
+
} else if (score >= 40) {
|
|
469
|
+
return {
|
|
470
|
+
level: 'medium',
|
|
471
|
+
description:
|
|
472
|
+
'Moderately structured. Could benefit from extracting pure logic.',
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
return {
|
|
476
|
+
level: 'low',
|
|
477
|
+
description:
|
|
478
|
+
'Tangled structure. Business logic mixed with I/O. Consider refactoring.',
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|