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,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Scorer Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the pure core quality scoring logic.
|
|
5
|
+
* These tests verify that quality scores are correctly computed
|
|
6
|
+
* for impure functions based on their structural patterns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
computeQualityScore,
|
|
13
|
+
analyzeFunction,
|
|
14
|
+
QUALITY_WEIGHTS,
|
|
15
|
+
} from '../src/classification/quality-scorer.js'
|
|
16
|
+
import type {
|
|
17
|
+
ExtractedFunction,
|
|
18
|
+
ImpurityMarker,
|
|
19
|
+
DetectionContext,
|
|
20
|
+
FileImports,
|
|
21
|
+
} from '../src/types.js'
|
|
22
|
+
import { createDetectionContext } from '../src/detection/detect-markers.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper to create a minimal extracted function for testing
|
|
26
|
+
*/
|
|
27
|
+
function createTestFunction(
|
|
28
|
+
overrides: Partial<ExtractedFunction> = {},
|
|
29
|
+
): ExtractedFunction {
|
|
30
|
+
return {
|
|
31
|
+
name: 'testFunction',
|
|
32
|
+
filePath: '/test/file.ts',
|
|
33
|
+
startLine: 1,
|
|
34
|
+
endLine: 10,
|
|
35
|
+
isAsync: true,
|
|
36
|
+
isExported: false,
|
|
37
|
+
bodyLineCount: 10,
|
|
38
|
+
statementCount: 5,
|
|
39
|
+
hasConditionals: false,
|
|
40
|
+
parentContext: null,
|
|
41
|
+
callSites: [],
|
|
42
|
+
hasAwait: true,
|
|
43
|
+
propertyAccessChains: [],
|
|
44
|
+
kind: 'function',
|
|
45
|
+
...overrides,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper to create a minimal detection context
|
|
51
|
+
*/
|
|
52
|
+
function createTestContext(
|
|
53
|
+
imports: FileImports['imports'] = [],
|
|
54
|
+
pureImports: string[] = [],
|
|
55
|
+
): DetectionContext {
|
|
56
|
+
const fileImports: FileImports = {
|
|
57
|
+
filePath: '/test/file.ts',
|
|
58
|
+
imports,
|
|
59
|
+
}
|
|
60
|
+
const context = createDetectionContext(fileImports)
|
|
61
|
+
|
|
62
|
+
// Add pure file imports
|
|
63
|
+
for (const pureImport of pureImports) {
|
|
64
|
+
context.pureFileImports.add(pureImport)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return context
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Helper to create markers
|
|
72
|
+
*/
|
|
73
|
+
function createMarkers(
|
|
74
|
+
types: Array<{ type: ImpurityMarker['type']; line?: number }>,
|
|
75
|
+
): ImpurityMarker[] {
|
|
76
|
+
return types.map(({ type, line }) => ({
|
|
77
|
+
type,
|
|
78
|
+
detail: `test ${type}`,
|
|
79
|
+
line,
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('computeQualityScore', () => {
|
|
84
|
+
describe('baseline scoring', () => {
|
|
85
|
+
it('should return a score between 0 and 100', () => {
|
|
86
|
+
const fn = createTestFunction()
|
|
87
|
+
const markers = createMarkers([{ type: 'await-expression', line: 5 }])
|
|
88
|
+
const context = createTestContext()
|
|
89
|
+
|
|
90
|
+
const score = computeQualityScore(fn, markers, context)
|
|
91
|
+
|
|
92
|
+
expect(score).toBeGreaterThanOrEqual(0)
|
|
93
|
+
expect(score).toBeLessThanOrEqual(100)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should start with neutral score of 50', () => {
|
|
97
|
+
const fn = createTestFunction({
|
|
98
|
+
bodyLineCount: 20,
|
|
99
|
+
statementCount: 10,
|
|
100
|
+
callSites: [],
|
|
101
|
+
})
|
|
102
|
+
const markers = createMarkers([{ type: 'await-expression', line: 10 }])
|
|
103
|
+
const context = createTestContext()
|
|
104
|
+
|
|
105
|
+
const score = computeQualityScore(fn, markers, context)
|
|
106
|
+
|
|
107
|
+
// Base score is 50, with various adjustments
|
|
108
|
+
// Without any positive signals, it should get penalties
|
|
109
|
+
expect(score).toBeLessThanOrEqual(50)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('positive signals', () => {
|
|
114
|
+
it('should give high score for calling .pure file imports', () => {
|
|
115
|
+
const fn = createTestFunction({
|
|
116
|
+
callSites: [
|
|
117
|
+
{ expression: 'planAcceptInvite', line: 5, isAwaited: false },
|
|
118
|
+
{ expression: 'db.user.findFirst', line: 2, isAwaited: true },
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
const markers = createMarkers([
|
|
122
|
+
{ type: 'database-call', line: 2 },
|
|
123
|
+
{ type: 'await-expression', line: 2 },
|
|
124
|
+
])
|
|
125
|
+
const context = createTestContext(
|
|
126
|
+
[
|
|
127
|
+
{
|
|
128
|
+
moduleSpecifier: './organizations.pure',
|
|
129
|
+
namedImports: ['planAcceptInvite'],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
['./organizations.pure'],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const score = computeQualityScore(fn, markers, context)
|
|
136
|
+
|
|
137
|
+
// Should get +30 for calling pure file
|
|
138
|
+
expect(score).toBeGreaterThanOrEqual(70)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should reward calling functions with pure naming conventions', () => {
|
|
142
|
+
const fn = createTestFunction({
|
|
143
|
+
callSites: [
|
|
144
|
+
{ expression: 'deriveSettings', line: 3, isAwaited: false },
|
|
145
|
+
{ expression: 'computeTotal', line: 4, isAwaited: false },
|
|
146
|
+
{ expression: 'db.user.update', line: 6, isAwaited: true },
|
|
147
|
+
],
|
|
148
|
+
})
|
|
149
|
+
const markers = createMarkers([{ type: 'database-call', line: 6 }])
|
|
150
|
+
const context = createTestContext()
|
|
151
|
+
|
|
152
|
+
const score = computeQualityScore(fn, markers, context)
|
|
153
|
+
|
|
154
|
+
// Should get +20 for pure naming conventions
|
|
155
|
+
expect(score).toBeGreaterThanOrEqual(60)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should reward calling predicate functions', () => {
|
|
159
|
+
const fn = createTestFunction({
|
|
160
|
+
callSites: [
|
|
161
|
+
{ expression: 'isValid', line: 2, isAwaited: false },
|
|
162
|
+
{ expression: 'hasPermission', line: 3, isAwaited: false },
|
|
163
|
+
{ expression: 'shouldProcess', line: 4, isAwaited: false },
|
|
164
|
+
{ expression: 'db.user.update', line: 6, isAwaited: true },
|
|
165
|
+
],
|
|
166
|
+
})
|
|
167
|
+
const markers = createMarkers([{ type: 'database-call', line: 6 }])
|
|
168
|
+
const context = createTestContext()
|
|
169
|
+
|
|
170
|
+
const score = computeQualityScore(fn, markers, context)
|
|
171
|
+
|
|
172
|
+
// Should get +5 for predicate functions
|
|
173
|
+
expect(score).toBeGreaterThanOrEqual(50)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should reward shell naming conventions', () => {
|
|
177
|
+
const fn = createTestFunction({
|
|
178
|
+
name: 'handleUserUpdate',
|
|
179
|
+
callSites: [{ expression: 'db.user.update', line: 5, isAwaited: true }],
|
|
180
|
+
})
|
|
181
|
+
const markers = createMarkers([{ type: 'database-call', line: 5 }])
|
|
182
|
+
const context = createTestContext()
|
|
183
|
+
|
|
184
|
+
const score = computeQualityScore(fn, markers, context)
|
|
185
|
+
|
|
186
|
+
// Should get +5 for shell naming
|
|
187
|
+
expect(score).toBeGreaterThanOrEqual(40)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should reward I/O concentrated at start (GATHER pattern)', () => {
|
|
191
|
+
const fn = createTestFunction({
|
|
192
|
+
bodyLineCount: 30,
|
|
193
|
+
callSites: [
|
|
194
|
+
{ expression: 'db.user.findFirst', line: 2, isAwaited: true },
|
|
195
|
+
{ expression: 'db.posts.findMany', line: 3, isAwaited: true },
|
|
196
|
+
{ expression: 'transform', line: 15, isAwaited: false },
|
|
197
|
+
{ expression: 'validate', line: 20, isAwaited: false },
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
const markers = createMarkers([
|
|
201
|
+
{ type: 'database-call', line: 2 },
|
|
202
|
+
{ type: 'database-call', line: 3 },
|
|
203
|
+
])
|
|
204
|
+
const context = createTestContext()
|
|
205
|
+
|
|
206
|
+
const score = computeQualityScore(fn, markers, context)
|
|
207
|
+
|
|
208
|
+
// Should get bonus for GATHER pattern
|
|
209
|
+
expect(score).toBeGreaterThanOrEqual(55)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should reward I/O concentrated at end (EXECUTE pattern)', () => {
|
|
213
|
+
const fn = createTestFunction({
|
|
214
|
+
bodyLineCount: 30,
|
|
215
|
+
callSites: [
|
|
216
|
+
{ expression: 'computeResult', line: 5, isAwaited: false },
|
|
217
|
+
{ expression: 'validateInput', line: 10, isAwaited: false },
|
|
218
|
+
{ expression: 'db.user.update', line: 25, isAwaited: true },
|
|
219
|
+
{ expression: 'db.audit.create', line: 28, isAwaited: true },
|
|
220
|
+
],
|
|
221
|
+
})
|
|
222
|
+
const markers = createMarkers([
|
|
223
|
+
{ type: 'database-call', line: 25 },
|
|
224
|
+
{ type: 'database-call', line: 28 },
|
|
225
|
+
])
|
|
226
|
+
const context = createTestContext()
|
|
227
|
+
|
|
228
|
+
const score = computeQualityScore(fn, markers, context)
|
|
229
|
+
|
|
230
|
+
// Should get bonus for EXECUTE pattern
|
|
231
|
+
expect(score).toBeGreaterThanOrEqual(55)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should reward low complexity functions', () => {
|
|
235
|
+
const fn = createTestFunction({
|
|
236
|
+
bodyLineCount: 10,
|
|
237
|
+
statementCount: 3,
|
|
238
|
+
hasConditionals: false,
|
|
239
|
+
callSites: [
|
|
240
|
+
{ expression: 'db.user.findFirst', line: 5, isAwaited: true },
|
|
241
|
+
],
|
|
242
|
+
})
|
|
243
|
+
const markers = createMarkers([{ type: 'database-call', line: 5 }])
|
|
244
|
+
const context = createTestContext()
|
|
245
|
+
|
|
246
|
+
const score = computeQualityScore(fn, markers, context)
|
|
247
|
+
|
|
248
|
+
// Should get +10 for low complexity
|
|
249
|
+
expect(score).toBeGreaterThanOrEqual(50)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('negative signals (penalties)', () => {
|
|
254
|
+
it('should penalize interleaved I/O', () => {
|
|
255
|
+
const fn = createTestFunction({
|
|
256
|
+
bodyLineCount: 30,
|
|
257
|
+
callSites: [
|
|
258
|
+
{ expression: 'db.user.findFirst', line: 3, isAwaited: true },
|
|
259
|
+
{ expression: 'transform', line: 8, isAwaited: false },
|
|
260
|
+
{ expression: 'db.posts.create', line: 12, isAwaited: true },
|
|
261
|
+
{ expression: 'validate', line: 18, isAwaited: false },
|
|
262
|
+
{ expression: 'db.audit.create', line: 25, isAwaited: true },
|
|
263
|
+
],
|
|
264
|
+
})
|
|
265
|
+
const markers = createMarkers([
|
|
266
|
+
{ type: 'database-call', line: 3 },
|
|
267
|
+
{ type: 'database-call', line: 12 },
|
|
268
|
+
{ type: 'database-call', line: 25 },
|
|
269
|
+
])
|
|
270
|
+
const context = createTestContext()
|
|
271
|
+
|
|
272
|
+
const score = computeQualityScore(fn, markers, context)
|
|
273
|
+
|
|
274
|
+
// Should get -20 for interleaved I/O
|
|
275
|
+
expect(score).toBeLessThanOrEqual(50)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should penalize high complexity functions', () => {
|
|
279
|
+
const fn = createTestFunction({
|
|
280
|
+
bodyLineCount: 150,
|
|
281
|
+
statementCount: 50,
|
|
282
|
+
hasConditionals: true,
|
|
283
|
+
callSites: [
|
|
284
|
+
{ expression: 'db.user.findFirst', line: 50, isAwaited: true },
|
|
285
|
+
],
|
|
286
|
+
})
|
|
287
|
+
const markers = createMarkers([{ type: 'database-call', line: 50 }])
|
|
288
|
+
const context = createTestContext()
|
|
289
|
+
|
|
290
|
+
const score = computeQualityScore(fn, markers, context)
|
|
291
|
+
|
|
292
|
+
// Should get -15 for high complexity
|
|
293
|
+
expect(score).toBeLessThanOrEqual(40)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should penalize multiple I/O types', () => {
|
|
297
|
+
const fn = createTestFunction({
|
|
298
|
+
callSites: [
|
|
299
|
+
{ expression: 'db.user.findFirst', line: 2, isAwaited: true },
|
|
300
|
+
{ expression: 'fetch', line: 5, isAwaited: true },
|
|
301
|
+
{ expression: 'fs.readFile', line: 8, isAwaited: true },
|
|
302
|
+
],
|
|
303
|
+
})
|
|
304
|
+
const markers = createMarkers([
|
|
305
|
+
{ type: 'database-call', line: 2 },
|
|
306
|
+
{ type: 'network-fetch', line: 5 },
|
|
307
|
+
{ type: 'fs-call', line: 8 },
|
|
308
|
+
])
|
|
309
|
+
const context = createTestContext()
|
|
310
|
+
|
|
311
|
+
const score = computeQualityScore(fn, markers, context)
|
|
312
|
+
|
|
313
|
+
// Should get -10 for multiple I/O types
|
|
314
|
+
expect(score).toBeLessThanOrEqual(50)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should penalize no pure function calls', () => {
|
|
318
|
+
const fn = createTestFunction({
|
|
319
|
+
callSites: [
|
|
320
|
+
{ expression: 'db.user.findFirst', line: 2, isAwaited: true },
|
|
321
|
+
{ expression: 'db.user.update', line: 5, isAwaited: true },
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
const markers = createMarkers([
|
|
325
|
+
{ type: 'database-call', line: 2 },
|
|
326
|
+
{ type: 'database-call', line: 5 },
|
|
327
|
+
])
|
|
328
|
+
const context = createTestContext()
|
|
329
|
+
|
|
330
|
+
const score = computeQualityScore(fn, markers, context)
|
|
331
|
+
|
|
332
|
+
// Should get -10 for no pure function calls
|
|
333
|
+
expect(score).toBeLessThanOrEqual(50)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should penalize very long functions', () => {
|
|
337
|
+
const fn = createTestFunction({
|
|
338
|
+
bodyLineCount: 150,
|
|
339
|
+
callSites: [
|
|
340
|
+
{ expression: 'db.user.findFirst', line: 50, isAwaited: true },
|
|
341
|
+
],
|
|
342
|
+
})
|
|
343
|
+
const markers = createMarkers([{ type: 'database-call', line: 50 }])
|
|
344
|
+
const context = createTestContext()
|
|
345
|
+
|
|
346
|
+
const score = computeQualityScore(fn, markers, context)
|
|
347
|
+
|
|
348
|
+
// Should get -10 for very long function
|
|
349
|
+
expect(score).toBeLessThanOrEqual(40)
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe('combined scenarios', () => {
|
|
354
|
+
it('should give high score for well-structured impure function', () => {
|
|
355
|
+
// This simulates the acceptInvite pattern from organizations.ts
|
|
356
|
+
// - Calls a pure function (planAcceptInvite)
|
|
357
|
+
// - I/O at the end (EXECUTE pattern)
|
|
358
|
+
// - Shell naming convention (handleX)
|
|
359
|
+
const fn = createTestFunction({
|
|
360
|
+
name: 'handleAcceptInvite',
|
|
361
|
+
bodyLineCount: 30,
|
|
362
|
+
statementCount: 8,
|
|
363
|
+
hasConditionals: true,
|
|
364
|
+
callSites: [
|
|
365
|
+
{ expression: 'planAcceptInvite', line: 5, isAwaited: false },
|
|
366
|
+
{ expression: 'db.invitation.update', line: 22, isAwaited: true },
|
|
367
|
+
{ expression: 'db.member.create', line: 25, isAwaited: true },
|
|
368
|
+
],
|
|
369
|
+
})
|
|
370
|
+
const markers = createMarkers([
|
|
371
|
+
{ type: 'database-call', line: 22 },
|
|
372
|
+
{ type: 'database-call', line: 25 },
|
|
373
|
+
])
|
|
374
|
+
const context = createTestContext(
|
|
375
|
+
[
|
|
376
|
+
{
|
|
377
|
+
moduleSpecifier: './organizations.pure',
|
|
378
|
+
namedImports: ['planAcceptInvite'],
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
['./organizations.pure'],
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const score = computeQualityScore(fn, markers, context)
|
|
385
|
+
|
|
386
|
+
expect(score).toBeGreaterThanOrEqual(70)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should give low score for tangled impure function', () => {
|
|
390
|
+
// This simulates a poorly structured function
|
|
391
|
+
// - No pure function calls
|
|
392
|
+
// - I/O interleaved throughout
|
|
393
|
+
// - High complexity
|
|
394
|
+
// - Multiple I/O types
|
|
395
|
+
const fn = createTestFunction({
|
|
396
|
+
name: 'processData',
|
|
397
|
+
bodyLineCount: 100,
|
|
398
|
+
statementCount: 40,
|
|
399
|
+
hasConditionals: true,
|
|
400
|
+
callSites: [
|
|
401
|
+
{ expression: 'db.user.findFirst', line: 5, isAwaited: true },
|
|
402
|
+
{ expression: 'fetch', line: 20, isAwaited: true },
|
|
403
|
+
{ expression: 'db.cache.get', line: 35, isAwaited: true },
|
|
404
|
+
{ expression: 'console.log', line: 50, isAwaited: false },
|
|
405
|
+
{ expression: 'db.result.create', line: 70, isAwaited: true },
|
|
406
|
+
{ expression: 'fetch', line: 85, isAwaited: true },
|
|
407
|
+
],
|
|
408
|
+
})
|
|
409
|
+
const markers = createMarkers([
|
|
410
|
+
{ type: 'database-call', line: 5 },
|
|
411
|
+
{ type: 'network-fetch', line: 20 },
|
|
412
|
+
{ type: 'database-call', line: 35 },
|
|
413
|
+
{ type: 'console-log', line: 50 },
|
|
414
|
+
{ type: 'database-call', line: 70 },
|
|
415
|
+
{ type: 'network-fetch', line: 85 },
|
|
416
|
+
])
|
|
417
|
+
const context = createTestContext()
|
|
418
|
+
|
|
419
|
+
const score = computeQualityScore(fn, markers, context)
|
|
420
|
+
|
|
421
|
+
expect(score).toBeLessThan(40)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('should give medium score for moderately structured function', () => {
|
|
425
|
+
// Some good patterns, some bad - interleaved I/O with no pure file calls
|
|
426
|
+
const fn = createTestFunction({
|
|
427
|
+
name: 'updateUser',
|
|
428
|
+
bodyLineCount: 100,
|
|
429
|
+
statementCount: 40,
|
|
430
|
+
hasConditionals: true,
|
|
431
|
+
callSites: [
|
|
432
|
+
{ expression: 'db.user.findFirst', line: 10, isAwaited: true },
|
|
433
|
+
{ expression: 'someHelper', line: 30, isAwaited: false },
|
|
434
|
+
{ expression: 'db.user.update', line: 50, isAwaited: true },
|
|
435
|
+
{ expression: 'anotherHelper', line: 70, isAwaited: false },
|
|
436
|
+
{ expression: 'db.audit.create', line: 90, isAwaited: true },
|
|
437
|
+
],
|
|
438
|
+
})
|
|
439
|
+
const markers = createMarkers([
|
|
440
|
+
{ type: 'database-call', line: 10 },
|
|
441
|
+
{ type: 'database-call', line: 50 },
|
|
442
|
+
{ type: 'database-call', line: 90 },
|
|
443
|
+
])
|
|
444
|
+
const context = createTestContext()
|
|
445
|
+
|
|
446
|
+
const score = computeQualityScore(fn, markers, context)
|
|
447
|
+
|
|
448
|
+
expect(score).toBeGreaterThanOrEqual(0)
|
|
449
|
+
expect(score).toBeLessThan(70)
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe('edge cases', () => {
|
|
454
|
+
it('should handle function with no call sites', () => {
|
|
455
|
+
const fn = createTestFunction({
|
|
456
|
+
callSites: [],
|
|
457
|
+
propertyAccessChains: ['process.env.NODE_ENV'],
|
|
458
|
+
})
|
|
459
|
+
const markers = createMarkers([{ type: 'env-access' }])
|
|
460
|
+
const context = createTestContext()
|
|
461
|
+
|
|
462
|
+
const score = computeQualityScore(fn, markers, context)
|
|
463
|
+
|
|
464
|
+
expect(score).toBeGreaterThanOrEqual(0)
|
|
465
|
+
expect(score).toBeLessThanOrEqual(100)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should handle function with single line', () => {
|
|
469
|
+
const fn = createTestFunction({
|
|
470
|
+
bodyLineCount: 1,
|
|
471
|
+
statementCount: 1,
|
|
472
|
+
callSites: [
|
|
473
|
+
{ expression: 'db.user.findFirst', line: 0, isAwaited: true },
|
|
474
|
+
],
|
|
475
|
+
})
|
|
476
|
+
const markers = createMarkers([{ type: 'database-call', line: 0 }])
|
|
477
|
+
const context = createTestContext()
|
|
478
|
+
|
|
479
|
+
const score = computeQualityScore(fn, markers, context)
|
|
480
|
+
|
|
481
|
+
expect(score).toBeGreaterThanOrEqual(0)
|
|
482
|
+
expect(score).toBeLessThanOrEqual(100)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('should clamp score to 0 for extremely bad functions', () => {
|
|
486
|
+
const fn = createTestFunction({
|
|
487
|
+
name: 'terribleFunction',
|
|
488
|
+
bodyLineCount: 500,
|
|
489
|
+
statementCount: 200,
|
|
490
|
+
hasConditionals: true,
|
|
491
|
+
callSites: Array.from({ length: 20 }, (_, i) => ({
|
|
492
|
+
expression:
|
|
493
|
+
i % 3 === 0
|
|
494
|
+
? 'db.user.findFirst'
|
|
495
|
+
: i % 3 === 1
|
|
496
|
+
? 'fetch'
|
|
497
|
+
: 'fs.readFile',
|
|
498
|
+
line: i * 25,
|
|
499
|
+
isAwaited: true,
|
|
500
|
+
})),
|
|
501
|
+
})
|
|
502
|
+
const markers = createMarkers([
|
|
503
|
+
...Array.from({ length: 7 }, () => ({
|
|
504
|
+
type: 'database-call' as const,
|
|
505
|
+
})),
|
|
506
|
+
...Array.from({ length: 7 }, () => ({
|
|
507
|
+
type: 'network-fetch' as const,
|
|
508
|
+
})),
|
|
509
|
+
...Array.from({ length: 6 }, () => ({ type: 'fs-call' as const })),
|
|
510
|
+
])
|
|
511
|
+
const context = createTestContext()
|
|
512
|
+
|
|
513
|
+
const score = computeQualityScore(fn, markers, context)
|
|
514
|
+
|
|
515
|
+
expect(score).toBeGreaterThanOrEqual(0)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('should clamp score to 100 for extremely good functions', () => {
|
|
519
|
+
const fn = createTestFunction({
|
|
520
|
+
name: 'handlePerfectFunction',
|
|
521
|
+
bodyLineCount: 10,
|
|
522
|
+
statementCount: 3,
|
|
523
|
+
hasConditionals: false,
|
|
524
|
+
callSites: [
|
|
525
|
+
{ expression: 'planOperation', line: 2, isAwaited: false },
|
|
526
|
+
{ expression: 'deriveResult', line: 3, isAwaited: false },
|
|
527
|
+
{ expression: 'computeValue', line: 4, isAwaited: false },
|
|
528
|
+
{ expression: 'isValid', line: 5, isAwaited: false },
|
|
529
|
+
{ expression: 'db.user.create', line: 8, isAwaited: true },
|
|
530
|
+
],
|
|
531
|
+
})
|
|
532
|
+
const markers = createMarkers([{ type: 'database-call', line: 8 }])
|
|
533
|
+
const context = createTestContext(
|
|
534
|
+
[{ moduleSpecifier: './domain.pure', namedImports: ['planOperation'] }],
|
|
535
|
+
['./domain.pure'],
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
const score = computeQualityScore(fn, markers, context)
|
|
539
|
+
|
|
540
|
+
expect(score).toBeLessThanOrEqual(100)
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('analyzeFunction', () => {
|
|
546
|
+
it('should return analysis with all expected fields', () => {
|
|
547
|
+
const fn = createTestFunction({
|
|
548
|
+
callSites: [
|
|
549
|
+
{ expression: 'db.user.findFirst', line: 5, isAwaited: true },
|
|
550
|
+
],
|
|
551
|
+
})
|
|
552
|
+
const markers = createMarkers([{ type: 'database-call', line: 5 }])
|
|
553
|
+
const context = createTestContext()
|
|
554
|
+
|
|
555
|
+
const analysis = analyzeFunction(fn, markers, context)
|
|
556
|
+
|
|
557
|
+
expect(analysis).toHaveProperty('callsPureFile')
|
|
558
|
+
expect(analysis).toHaveProperty('callsPureNamingConvention')
|
|
559
|
+
expect(analysis).toHaveProperty('ioConcentratedAtStart')
|
|
560
|
+
expect(analysis).toHaveProperty('ioConcentratedAtEnd')
|
|
561
|
+
expect(analysis).toHaveProperty('lowComplexity')
|
|
562
|
+
expect(analysis).toHaveProperty('shellNamingConvention')
|
|
563
|
+
expect(analysis).toHaveProperty('callsPredicateFunctions')
|
|
564
|
+
expect(analysis).toHaveProperty('ioInterleaved')
|
|
565
|
+
expect(analysis).toHaveProperty('highComplexity')
|
|
566
|
+
expect(analysis).toHaveProperty('multipleIoTypes')
|
|
567
|
+
expect(analysis).toHaveProperty('noPureFunctionCalls')
|
|
568
|
+
expect(analysis).toHaveProperty('veryLongFunction')
|
|
569
|
+
expect(analysis).toHaveProperty('estimatedComplexity')
|
|
570
|
+
expect(analysis).toHaveProperty('ioMarkerCount')
|
|
571
|
+
expect(analysis).toHaveProperty('uniqueIoTypes')
|
|
572
|
+
expect(analysis).toHaveProperty('pureCallCount')
|
|
573
|
+
expect(analysis).toHaveProperty('predicateCallCount')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('should detect pure file imports correctly', () => {
|
|
577
|
+
const fn = createTestFunction({
|
|
578
|
+
callSites: [
|
|
579
|
+
{ expression: 'planAcceptInvite', line: 5, isAwaited: false },
|
|
580
|
+
],
|
|
581
|
+
})
|
|
582
|
+
const markers: ImpurityMarker[] = []
|
|
583
|
+
const context = createTestContext(
|
|
584
|
+
[
|
|
585
|
+
{
|
|
586
|
+
moduleSpecifier: './organizations.pure',
|
|
587
|
+
namedImports: ['planAcceptInvite'],
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
['./organizations.pure'],
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
const analysis = analyzeFunction(fn, markers, context)
|
|
594
|
+
|
|
595
|
+
expect(analysis.callsPureFile).toBe(true)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('should detect pure naming conventions', () => {
|
|
599
|
+
const fn = createTestFunction({
|
|
600
|
+
callSites: [
|
|
601
|
+
{ expression: 'deriveSettings', line: 2, isAwaited: false },
|
|
602
|
+
{ expression: 'computeTotal', line: 3, isAwaited: false },
|
|
603
|
+
{ expression: 'transformData', line: 4, isAwaited: false },
|
|
604
|
+
],
|
|
605
|
+
})
|
|
606
|
+
const markers: ImpurityMarker[] = []
|
|
607
|
+
const context = createTestContext()
|
|
608
|
+
|
|
609
|
+
const analysis = analyzeFunction(fn, markers, context)
|
|
610
|
+
|
|
611
|
+
expect(analysis.callsPureNamingConvention).toBe(true)
|
|
612
|
+
expect(analysis.pureCallCount).toBe(3)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should detect predicate function calls', () => {
|
|
616
|
+
const fn = createTestFunction({
|
|
617
|
+
callSites: [
|
|
618
|
+
{ expression: 'isValid', line: 2, isAwaited: false },
|
|
619
|
+
{ expression: 'hasPermission', line: 3, isAwaited: false },
|
|
620
|
+
],
|
|
621
|
+
})
|
|
622
|
+
const markers: ImpurityMarker[] = []
|
|
623
|
+
const context = createTestContext()
|
|
624
|
+
|
|
625
|
+
const analysis = analyzeFunction(fn, markers, context)
|
|
626
|
+
|
|
627
|
+
expect(analysis.callsPredicateFunctions).toBe(true)
|
|
628
|
+
expect(analysis.predicateCallCount).toBe(2)
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
describe('QUALITY_WEIGHTS', () => {
|
|
633
|
+
it('should have positive weights for good patterns', () => {
|
|
634
|
+
expect(QUALITY_WEIGHTS.callsPureFile).toBeGreaterThan(0)
|
|
635
|
+
expect(QUALITY_WEIGHTS.callsPureNamingConvention).toBeGreaterThan(0)
|
|
636
|
+
expect(QUALITY_WEIGHTS.ioConcentratedAtStart).toBeGreaterThan(0)
|
|
637
|
+
expect(QUALITY_WEIGHTS.ioConcentratedAtEnd).toBeGreaterThan(0)
|
|
638
|
+
expect(QUALITY_WEIGHTS.lowComplexity).toBeGreaterThan(0)
|
|
639
|
+
expect(QUALITY_WEIGHTS.shellNamingConvention).toBeGreaterThan(0)
|
|
640
|
+
expect(QUALITY_WEIGHTS.callsPredicateFunctions).toBeGreaterThan(0)
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('should have negative weights for bad patterns', () => {
|
|
644
|
+
expect(QUALITY_WEIGHTS.ioInterleaved).toBeLessThan(0)
|
|
645
|
+
expect(QUALITY_WEIGHTS.highComplexity).toBeLessThan(0)
|
|
646
|
+
expect(QUALITY_WEIGHTS.multipleIoTypes).toBeLessThan(0)
|
|
647
|
+
expect(QUALITY_WEIGHTS.noPureFunctionCalls).toBeLessThan(0)
|
|
648
|
+
expect(QUALITY_WEIGHTS.veryLongFunction).toBeLessThan(0)
|
|
649
|
+
})
|
|
650
|
+
})
|