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
package/src/analyzer.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzer - Main Orchestration Layer (Shell)
|
|
3
|
+
*
|
|
4
|
+
* This module orchestrates the full analysis pipeline:
|
|
5
|
+
* 1. Load project via ts-morph
|
|
6
|
+
* 2. Extract functions from source files
|
|
7
|
+
* 3. Detect impurity markers
|
|
8
|
+
* 4. Classify functions
|
|
9
|
+
* 5. Compute quality scores
|
|
10
|
+
* 6. Aggregate into file, directory, and project scores
|
|
11
|
+
*
|
|
12
|
+
* This is a SHELL module - it coordinates I/O and calls pure functions.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SourceFile } from 'ts-morph'
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AnalysisError,
|
|
19
|
+
AnalyzerConfig,
|
|
20
|
+
ClassifiedFunction,
|
|
21
|
+
DirectoryScore,
|
|
22
|
+
FileScore,
|
|
23
|
+
ProjectScore,
|
|
24
|
+
} from './types.js'
|
|
25
|
+
import { DEFAULT_QUALITY_THRESHOLDS } from './types.js'
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
extractFunctions,
|
|
29
|
+
extractImports,
|
|
30
|
+
isTypeOnlyFile,
|
|
31
|
+
} from './extraction/extract-functions.js'
|
|
32
|
+
import { loadProject, getCommitHash } from './extraction/extractor.js'
|
|
33
|
+
import {
|
|
34
|
+
detectMarkers,
|
|
35
|
+
createDetectionContext,
|
|
36
|
+
} from './detection/detect-markers.js'
|
|
37
|
+
import {
|
|
38
|
+
classifyFunction,
|
|
39
|
+
shouldExcludeFunction,
|
|
40
|
+
createClassifiedFunction,
|
|
41
|
+
} from './classification/classifier.js'
|
|
42
|
+
import { computeQualityScore } from './classification/quality-scorer.js'
|
|
43
|
+
import { deriveStatus } from './classification/derive-status.js'
|
|
44
|
+
import {
|
|
45
|
+
scoreFile,
|
|
46
|
+
scoreDirectory,
|
|
47
|
+
scoreProject,
|
|
48
|
+
groupFilesByDirectory,
|
|
49
|
+
} from './scoring/scorer.js'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the full analysis pipeline
|
|
53
|
+
*
|
|
54
|
+
* @param config - Analyzer configuration
|
|
55
|
+
* @returns Project score with all metrics
|
|
56
|
+
*/
|
|
57
|
+
export async function analyze(config: AnalyzerConfig): Promise<ProjectScore> {
|
|
58
|
+
const errors: AnalysisError[] = []
|
|
59
|
+
|
|
60
|
+
// Step 1: Load project
|
|
61
|
+
const extractorOptions: { tsconfigPath: string; filesGlob?: string } = {
|
|
62
|
+
tsconfigPath: config.tsconfigPath,
|
|
63
|
+
}
|
|
64
|
+
if (config.filesGlob !== undefined) {
|
|
65
|
+
extractorOptions.filesGlob = config.filesGlob
|
|
66
|
+
}
|
|
67
|
+
const { sourceFiles } = loadProject(extractorOptions)
|
|
68
|
+
|
|
69
|
+
if (sourceFiles.length === 0) {
|
|
70
|
+
throw new Error('No source files found to analyze')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 2-5: Extract, detect, classify each file
|
|
74
|
+
const fileScores: FileScore[] = []
|
|
75
|
+
|
|
76
|
+
for (const sourceFile of sourceFiles) {
|
|
77
|
+
try {
|
|
78
|
+
const fileScore = analyzeFile(sourceFile)
|
|
79
|
+
fileScores.push(fileScore)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
errors.push({
|
|
82
|
+
filePath: sourceFile.getFilePath(),
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if any files were successfully analyzed
|
|
89
|
+
if (fileScores.length === 0 && errors.length > 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Failed to analyze any files. ${errors.length} files had errors.`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 6: Aggregate into directory scores
|
|
96
|
+
const directoryGroups = groupFilesByDirectory(fileScores)
|
|
97
|
+
const directoryScores: DirectoryScore[] = []
|
|
98
|
+
|
|
99
|
+
for (const [dirPath, files] of directoryGroups) {
|
|
100
|
+
const dirScore = scoreDirectory(dirPath, files)
|
|
101
|
+
directoryScores.push(dirScore)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort directories by path for consistent output
|
|
105
|
+
directoryScores.sort((a, b) => a.dirPath.localeCompare(b.dirPath))
|
|
106
|
+
|
|
107
|
+
// Step 7: Aggregate into project score
|
|
108
|
+
const projectOptions: {
|
|
109
|
+
timestamp?: string
|
|
110
|
+
commitHash?: string | null
|
|
111
|
+
subset?: boolean
|
|
112
|
+
filesGlob?: string
|
|
113
|
+
errors?: { filePath: string; error: string }[]
|
|
114
|
+
} = {
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
commitHash: getCommitHash(),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (config.filesGlob !== undefined) {
|
|
120
|
+
projectOptions.subset = true
|
|
121
|
+
projectOptions.filesGlob = config.filesGlob
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (errors.length > 0) {
|
|
125
|
+
projectOptions.errors = errors
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const projectScore = scoreProject(directoryScores, projectOptions)
|
|
129
|
+
|
|
130
|
+
return projectScore
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Analyze a single source file
|
|
135
|
+
*
|
|
136
|
+
* @param sourceFile - The ts-morph source file to analyze
|
|
137
|
+
* @returns FileScore with all metrics
|
|
138
|
+
*/
|
|
139
|
+
export function analyzeFile(sourceFile: SourceFile): FileScore {
|
|
140
|
+
const filePath = sourceFile.getFilePath()
|
|
141
|
+
|
|
142
|
+
// Check if type-only file
|
|
143
|
+
if (isTypeOnlyFile(sourceFile)) {
|
|
144
|
+
return scoreFile(filePath, [], true)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Extract functions and imports
|
|
148
|
+
const functions = extractFunctions(sourceFile)
|
|
149
|
+
const imports = extractImports(sourceFile)
|
|
150
|
+
|
|
151
|
+
// Create detection context
|
|
152
|
+
const context = createDetectionContext(imports)
|
|
153
|
+
|
|
154
|
+
// Classify each function
|
|
155
|
+
const classifiedFunctions: ClassifiedFunction[] = []
|
|
156
|
+
|
|
157
|
+
for (const fn of functions) {
|
|
158
|
+
// Check if function should be excluded
|
|
159
|
+
if (shouldExcludeFunction(fn)) {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Detect markers
|
|
164
|
+
const markers = detectMarkers(fn, context)
|
|
165
|
+
|
|
166
|
+
// Classify
|
|
167
|
+
const classification = classifyFunction(fn, markers)
|
|
168
|
+
|
|
169
|
+
// Compute quality score (only for impure functions)
|
|
170
|
+
let qualityScore: number | null = null
|
|
171
|
+
if (classification === 'impure') {
|
|
172
|
+
qualityScore = computeQualityScore(fn, markers, context)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Derive status
|
|
176
|
+
const status = deriveStatus(
|
|
177
|
+
classification,
|
|
178
|
+
qualityScore,
|
|
179
|
+
DEFAULT_QUALITY_THRESHOLDS,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
// Create classified function
|
|
183
|
+
const classifiedFn = createClassifiedFunction(
|
|
184
|
+
fn,
|
|
185
|
+
markers,
|
|
186
|
+
qualityScore,
|
|
187
|
+
status,
|
|
188
|
+
)
|
|
189
|
+
classifiedFunctions.push(classifiedFn)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Score the file
|
|
193
|
+
return scoreFile(filePath, classifiedFunctions, false)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Analyze a single file from a file path (convenience function)
|
|
198
|
+
*
|
|
199
|
+
* @param tsconfigPath - Path to tsconfig.json
|
|
200
|
+
* @param filePath - Path to the file to analyze
|
|
201
|
+
* @returns FileScore with all metrics
|
|
202
|
+
*/
|
|
203
|
+
export async function analyzeFilePath(
|
|
204
|
+
tsconfigPath: string,
|
|
205
|
+
filePath: string,
|
|
206
|
+
): Promise<FileScore | null> {
|
|
207
|
+
const { sourceFiles } = loadProject({
|
|
208
|
+
tsconfigPath,
|
|
209
|
+
filesGlob: filePath,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
if (sourceFiles.length === 0) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sourceFile = sourceFiles[0]
|
|
217
|
+
if (!sourceFile) {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return analyzeFile(sourceFile)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if analysis passes thresholds
|
|
226
|
+
*
|
|
227
|
+
* @param score - Project score to check
|
|
228
|
+
* @param config - Configuration with threshold values
|
|
229
|
+
* @returns Object indicating pass/fail status and messages
|
|
230
|
+
*/
|
|
231
|
+
export function checkThresholds(
|
|
232
|
+
score: ProjectScore,
|
|
233
|
+
config: Pick<AnalyzerConfig, 'minHealth' | 'minPurity' | 'minQuality'>,
|
|
234
|
+
): {
|
|
235
|
+
passed: boolean
|
|
236
|
+
failures: string[]
|
|
237
|
+
} {
|
|
238
|
+
const failures: string[] = []
|
|
239
|
+
|
|
240
|
+
if (config.minHealth !== undefined && score.health < config.minHealth) {
|
|
241
|
+
failures.push(
|
|
242
|
+
`Health ${score.health.toFixed(1)}% is below minimum ${config.minHealth}%`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (config.minPurity !== undefined && score.purity < config.minPurity) {
|
|
247
|
+
failures.push(
|
|
248
|
+
`Purity ${score.purity.toFixed(1)}% is below minimum ${config.minPurity}%`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
config.minQuality !== undefined &&
|
|
254
|
+
score.impurityQuality !== null &&
|
|
255
|
+
score.impurityQuality < config.minQuality
|
|
256
|
+
) {
|
|
257
|
+
failures.push(
|
|
258
|
+
`Impurity Quality ${score.impurityQuality.toFixed(1)}% is below minimum ${config.minQuality}%`,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
passed: failures.length === 0,
|
|
264
|
+
failures,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function Classifier - Pure Core
|
|
3
|
+
*
|
|
4
|
+
* Classifies functions as pure or impure based on their markers.
|
|
5
|
+
* This is a PURE module - all functions take data in and return data out.
|
|
6
|
+
*
|
|
7
|
+
* Classification is binary and objective:
|
|
8
|
+
* - Pure: Zero I/O markers
|
|
9
|
+
* - Impure: Has one or more I/O markers
|
|
10
|
+
*
|
|
11
|
+
* Note: `async` without `await` (and without other I/O markers) is still pure.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ClassifiedFunction,
|
|
16
|
+
ExtractedFunction,
|
|
17
|
+
FunctionClassification,
|
|
18
|
+
ImpurityMarker,
|
|
19
|
+
Status,
|
|
20
|
+
} from '../types.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimum statements for a function to be considered for scoring
|
|
24
|
+
* Functions with fewer statements are considered trivial
|
|
25
|
+
*/
|
|
26
|
+
const TRIVIAL_STATEMENT_THRESHOLD = 3
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Classify a function based on its markers
|
|
30
|
+
*
|
|
31
|
+
* @param fn - The extracted function
|
|
32
|
+
* @param markers - Detected impurity markers
|
|
33
|
+
* @returns The function classification ('pure' or 'impure')
|
|
34
|
+
*/
|
|
35
|
+
export function classifyFunction(
|
|
36
|
+
_fn: ExtractedFunction,
|
|
37
|
+
markers: ImpurityMarker[],
|
|
38
|
+
): FunctionClassification {
|
|
39
|
+
// A function is pure if it has no impurity markers
|
|
40
|
+
if (markers.length === 0) {
|
|
41
|
+
return 'pure'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'impure'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a function should be excluded from scoring
|
|
49
|
+
*
|
|
50
|
+
* Excluded functions:
|
|
51
|
+
* - Trivial functions (< 3 statements and no conditionals)
|
|
52
|
+
* - Test files (should already be filtered at extraction)
|
|
53
|
+
* - Type-only files (should already be filtered at extraction)
|
|
54
|
+
*/
|
|
55
|
+
export function shouldExcludeFunction(fn: ExtractedFunction): boolean {
|
|
56
|
+
// Exclude trivial functions: less than threshold statements and no conditionals
|
|
57
|
+
if (fn.statementCount < TRIVIAL_STATEMENT_THRESHOLD && !fn.hasConditionals) {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a fully classified function with all computed properties
|
|
66
|
+
*
|
|
67
|
+
* @param fn - The extracted function
|
|
68
|
+
* @param markers - Detected impurity markers
|
|
69
|
+
* @param qualityScore - Quality score (null for pure functions)
|
|
70
|
+
* @param status - Derived status
|
|
71
|
+
* @returns A fully classified function
|
|
72
|
+
*/
|
|
73
|
+
export function createClassifiedFunction(
|
|
74
|
+
fn: ExtractedFunction,
|
|
75
|
+
markers: ImpurityMarker[],
|
|
76
|
+
qualityScore: number | null,
|
|
77
|
+
status: Status,
|
|
78
|
+
): ClassifiedFunction {
|
|
79
|
+
const classification = classifyFunction(fn, markers)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...fn,
|
|
83
|
+
markers,
|
|
84
|
+
classification,
|
|
85
|
+
qualityScore,
|
|
86
|
+
status,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Batch classify multiple functions
|
|
92
|
+
*
|
|
93
|
+
* @param functions - Array of extracted functions with their markers
|
|
94
|
+
* @returns Array of classified functions
|
|
95
|
+
*/
|
|
96
|
+
export function classifyFunctions(
|
|
97
|
+
functions: Array<{
|
|
98
|
+
fn: ExtractedFunction
|
|
99
|
+
markers: ImpurityMarker[]
|
|
100
|
+
qualityScore: number | null
|
|
101
|
+
status: Status
|
|
102
|
+
}>,
|
|
103
|
+
): ClassifiedFunction[] {
|
|
104
|
+
return functions.map(({ fn, markers, qualityScore, status }) =>
|
|
105
|
+
createClassifiedFunction(fn, markers, qualityScore, status),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Partition functions into pure and impure
|
|
111
|
+
*
|
|
112
|
+
* @param functions - Array of classified functions
|
|
113
|
+
* @returns Object with pure and impure function arrays
|
|
114
|
+
*/
|
|
115
|
+
export function partitionByClassification(functions: ClassifiedFunction[]): {
|
|
116
|
+
pure: ClassifiedFunction[]
|
|
117
|
+
impure: ClassifiedFunction[]
|
|
118
|
+
} {
|
|
119
|
+
const pure: ClassifiedFunction[] = []
|
|
120
|
+
const impure: ClassifiedFunction[] = []
|
|
121
|
+
|
|
122
|
+
for (const fn of functions) {
|
|
123
|
+
if (fn.classification === 'pure') {
|
|
124
|
+
pure.push(fn)
|
|
125
|
+
} else {
|
|
126
|
+
impure.push(fn)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { pure, impure }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get classification summary for a set of functions
|
|
135
|
+
*
|
|
136
|
+
* @param functions - Array of classified functions
|
|
137
|
+
* @returns Summary statistics
|
|
138
|
+
*/
|
|
139
|
+
export function getClassificationSummary(functions: ClassifiedFunction[]): {
|
|
140
|
+
total: number
|
|
141
|
+
pureCount: number
|
|
142
|
+
impureCount: number
|
|
143
|
+
purityPercentage: number
|
|
144
|
+
} {
|
|
145
|
+
const total = functions.length
|
|
146
|
+
const pureCount = functions.filter(f => f.classification === 'pure').length
|
|
147
|
+
const impureCount = total - pureCount
|
|
148
|
+
const purityPercentage = total > 0 ? (pureCount / total) * 100 : 100
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
total,
|
|
152
|
+
pureCount,
|
|
153
|
+
impureCount,
|
|
154
|
+
purityPercentage,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Derivation - Pure Core
|
|
3
|
+
*
|
|
4
|
+
* Derives actionable status from classification and quality score.
|
|
5
|
+
* This is a PURE module - all functions take data in and return data out with no I/O.
|
|
6
|
+
*
|
|
7
|
+
* Status provides developer guidance:
|
|
8
|
+
* - ok: No action needed (pure function, or well-structured impure function)
|
|
9
|
+
* - review: Consider improving if touching this code
|
|
10
|
+
* - refactor: Tangled code, prioritize cleanup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ClassifiedFunction,
|
|
15
|
+
FunctionClassification,
|
|
16
|
+
QualityThresholds,
|
|
17
|
+
Status,
|
|
18
|
+
} from '../types.js'
|
|
19
|
+
import { DEFAULT_QUALITY_THRESHOLDS } from '../types.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Derive status from classification and quality score
|
|
23
|
+
*
|
|
24
|
+
* Status rules:
|
|
25
|
+
* - Pure functions always get 'ok' (testable without mocks)
|
|
26
|
+
* - Impure functions with quality >= okThreshold (default 70) get 'ok'
|
|
27
|
+
* - Impure functions with quality >= reviewThreshold (default 40) get 'review'
|
|
28
|
+
* - Impure functions with quality < reviewThreshold get 'refactor'
|
|
29
|
+
*
|
|
30
|
+
* @param classification - 'pure' or 'impure'
|
|
31
|
+
* @param qualityScore - Quality score for impure functions (0-100), null for pure
|
|
32
|
+
* @param thresholds - Optional custom thresholds
|
|
33
|
+
* @returns Status: 'ok' | 'review' | 'refactor'
|
|
34
|
+
*/
|
|
35
|
+
export function deriveStatus(
|
|
36
|
+
classification: FunctionClassification,
|
|
37
|
+
qualityScore: number | null,
|
|
38
|
+
thresholds: QualityThresholds = DEFAULT_QUALITY_THRESHOLDS,
|
|
39
|
+
): Status {
|
|
40
|
+
// Pure functions are always ok - testable without mocks
|
|
41
|
+
if (classification === 'pure') {
|
|
42
|
+
return 'ok'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Impure functions need quality score evaluation
|
|
46
|
+
if (qualityScore === null) {
|
|
47
|
+
// This shouldn't happen for impure functions, but handle defensively
|
|
48
|
+
return 'review'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (qualityScore >= thresholds.okThreshold) {
|
|
52
|
+
return 'ok'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (qualityScore >= thresholds.reviewThreshold) {
|
|
56
|
+
return 'review'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'refactor'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get status description for display
|
|
64
|
+
*/
|
|
65
|
+
export function getStatusDescription(status: Status): string {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case 'ok':
|
|
68
|
+
return 'No action needed'
|
|
69
|
+
case 'review':
|
|
70
|
+
return 'Consider improving if touching this code'
|
|
71
|
+
case 'refactor':
|
|
72
|
+
return 'Tangled code, prioritize cleanup'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get status emoji for display
|
|
78
|
+
*/
|
|
79
|
+
export function getStatusEmoji(status: Status): string {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case 'ok':
|
|
82
|
+
return '✓'
|
|
83
|
+
case 'review':
|
|
84
|
+
return '◐'
|
|
85
|
+
case 'refactor':
|
|
86
|
+
return '✗'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get status color for terminal display
|
|
92
|
+
*/
|
|
93
|
+
export function getStatusColor(status: Status): 'green' | 'yellow' | 'red' {
|
|
94
|
+
switch (status) {
|
|
95
|
+
case 'ok':
|
|
96
|
+
return 'green'
|
|
97
|
+
case 'review':
|
|
98
|
+
return 'yellow'
|
|
99
|
+
case 'refactor':
|
|
100
|
+
return 'red'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if a classified function should be flagged for attention
|
|
106
|
+
* (status is 'review' or 'refactor')
|
|
107
|
+
*/
|
|
108
|
+
export function needsAttention(fn: ClassifiedFunction): boolean {
|
|
109
|
+
return fn.status === 'review' || fn.status === 'refactor'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a classified function is in good standing
|
|
114
|
+
* (status is 'ok')
|
|
115
|
+
*/
|
|
116
|
+
export function isOk(fn: ClassifiedFunction): boolean {
|
|
117
|
+
return fn.status === 'ok'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Sort functions by status priority (refactor first, then review, then ok)
|
|
122
|
+
*/
|
|
123
|
+
export function sortByStatusPriority(
|
|
124
|
+
functions: ClassifiedFunction[],
|
|
125
|
+
): ClassifiedFunction[] {
|
|
126
|
+
const priorityMap: Record<Status, number> = {
|
|
127
|
+
refactor: 0,
|
|
128
|
+
review: 1,
|
|
129
|
+
ok: 2,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [...functions].sort((a, b) => {
|
|
133
|
+
const priorityDiff = priorityMap[a.status] - priorityMap[b.status]
|
|
134
|
+
if (priorityDiff !== 0) return priorityDiff
|
|
135
|
+
|
|
136
|
+
// Secondary sort by quality score (lower first for impure functions)
|
|
137
|
+
if (a.qualityScore !== null && b.qualityScore !== null) {
|
|
138
|
+
return a.qualityScore - b.qualityScore
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Tertiary sort by body line count (larger first)
|
|
142
|
+
return b.bodyLineCount - a.bodyLineCount
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate threshold configuration
|
|
148
|
+
*/
|
|
149
|
+
export function validateThresholds(thresholds: QualityThresholds): {
|
|
150
|
+
valid: boolean
|
|
151
|
+
errors: string[]
|
|
152
|
+
} {
|
|
153
|
+
const errors: string[] = []
|
|
154
|
+
|
|
155
|
+
if (thresholds.okThreshold < 0 || thresholds.okThreshold > 100) {
|
|
156
|
+
errors.push('okThreshold must be between 0 and 100')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (thresholds.reviewThreshold < 0 || thresholds.reviewThreshold > 100) {
|
|
160
|
+
errors.push('reviewThreshold must be between 0 and 100')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (thresholds.reviewThreshold >= thresholds.okThreshold) {
|
|
164
|
+
errors.push('reviewThreshold must be less than okThreshold')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
valid: errors.length === 0,
|
|
169
|
+
errors,
|
|
170
|
+
}
|
|
171
|
+
}
|