@aiready/testability 0.4.16 → 0.4.18
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-test.log +4 -4
- package/README.md +6 -0
- package/dist/chunk-P53QYYIW.mjs +258 -0
- package/dist/chunk-QOIBI5E7.mjs +262 -0
- package/dist/chunk-RQRAKTO6.mjs +251 -0
- package/dist/chunk-U2VGBMYR.mjs +259 -0
- package/dist/chunk-UT24GZ66.mjs +254 -0
- package/dist/chunk-VDC6AKUD.mjs +257 -0
- package/dist/chunk-VNU3SQOC.mjs +265 -0
- package/dist/cli.js +63 -144
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.d.ts +0 -12
- package/dist/index.js +63 -144
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +70 -216
package/dist/index.js
CHANGED
|
@@ -34,82 +34,7 @@ var import_core3 = require("@aiready/core");
|
|
|
34
34
|
var import_core = require("@aiready/core");
|
|
35
35
|
var import_fs = require("fs");
|
|
36
36
|
var import_path = require("path");
|
|
37
|
-
|
|
38
|
-
function countMethodsInInterface(node) {
|
|
39
|
-
if (node.type === "TSInterfaceDeclaration") {
|
|
40
|
-
return node.body.body.filter(
|
|
41
|
-
(m) => m.type === "TSMethodSignature" || m.type === "TSPropertySignature"
|
|
42
|
-
).length;
|
|
43
|
-
}
|
|
44
|
-
if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") {
|
|
45
|
-
return node.typeAnnotation.members.length;
|
|
46
|
-
}
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
function hasDependencyInjection(node) {
|
|
50
|
-
for (const member of node.body.body) {
|
|
51
|
-
if (member.type === "MethodDefinition" && member.key.type === "Identifier" && member.key.name === "constructor") {
|
|
52
|
-
const fn = member.value;
|
|
53
|
-
if (fn.params && fn.params.length > 0) {
|
|
54
|
-
const typedParams = fn.params.filter((p) => {
|
|
55
|
-
const param = p;
|
|
56
|
-
return param.typeAnnotation != null || param.parameter?.typeAnnotation != null;
|
|
57
|
-
});
|
|
58
|
-
if (typedParams.length > 0) return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
function isPureFunction(fn) {
|
|
65
|
-
let hasReturn = false;
|
|
66
|
-
let hasSideEffect = false;
|
|
67
|
-
function walk(node) {
|
|
68
|
-
if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
|
|
69
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
70
|
-
hasSideEffect = true;
|
|
71
|
-
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
|
|
72
|
-
node.callee.object.name
|
|
73
|
-
))
|
|
74
|
-
hasSideEffect = true;
|
|
75
|
-
for (const key of Object.keys(node)) {
|
|
76
|
-
if (key === "parent") continue;
|
|
77
|
-
const child = node[key];
|
|
78
|
-
if (child && typeof child === "object") {
|
|
79
|
-
if (Array.isArray(child)) {
|
|
80
|
-
child.forEach((c) => c?.type && walk(c));
|
|
81
|
-
} else if (child.type) {
|
|
82
|
-
walk(child);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (fn.body?.type === "BlockStatement") {
|
|
88
|
-
fn.body.body.forEach((s) => walk(s));
|
|
89
|
-
} else if (fn.body) {
|
|
90
|
-
hasReturn = true;
|
|
91
|
-
}
|
|
92
|
-
return hasReturn && !hasSideEffect;
|
|
93
|
-
}
|
|
94
|
-
function hasExternalStateMutation(fn) {
|
|
95
|
-
let found = false;
|
|
96
|
-
function walk(node) {
|
|
97
|
-
if (found) return;
|
|
98
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
99
|
-
found = true;
|
|
100
|
-
for (const key of Object.keys(node)) {
|
|
101
|
-
if (key === "parent") continue;
|
|
102
|
-
const child = node[key];
|
|
103
|
-
if (child && typeof child === "object") {
|
|
104
|
-
if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
|
|
105
|
-
else if (child.type) walk(child);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (fn.body?.type === "BlockStatement") fn.body.body.forEach((s) => walk(s));
|
|
110
|
-
return found;
|
|
111
|
-
}
|
|
112
|
-
function analyzeFileTestability(filePath) {
|
|
37
|
+
async function analyzeFileTestability(filePath) {
|
|
113
38
|
const result = {
|
|
114
39
|
pureFunctions: 0,
|
|
115
40
|
totalFunctions: 0,
|
|
@@ -119,75 +44,79 @@ function analyzeFileTestability(filePath) {
|
|
|
119
44
|
totalInterfaces: 0,
|
|
120
45
|
externalStateMutations: 0
|
|
121
46
|
};
|
|
47
|
+
const parser = (0, import_core.getParser)(filePath);
|
|
48
|
+
if (!parser) return result;
|
|
122
49
|
let code;
|
|
123
50
|
try {
|
|
124
51
|
code = (0, import_fs.readFileSync)(filePath, "utf-8");
|
|
125
52
|
} catch {
|
|
126
53
|
return result;
|
|
127
54
|
}
|
|
128
|
-
let ast;
|
|
129
55
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (key === "parent") continue;
|
|
155
|
-
const child = node[key];
|
|
156
|
-
if (child && typeof child === "object") {
|
|
157
|
-
if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
|
|
158
|
-
else if (child.type) visit(child);
|
|
56
|
+
await parser.initialize();
|
|
57
|
+
const parseResult = parser.parse(code, filePath);
|
|
58
|
+
for (const exp of parseResult.exports) {
|
|
59
|
+
if (exp.type === "function") {
|
|
60
|
+
result.totalFunctions++;
|
|
61
|
+
if (exp.isPure) result.pureFunctions++;
|
|
62
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
63
|
+
}
|
|
64
|
+
if (exp.type === "class") {
|
|
65
|
+
result.totalClasses++;
|
|
66
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
67
|
+
result.injectionPatterns++;
|
|
68
|
+
}
|
|
69
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
70
|
+
if (total > 5) {
|
|
71
|
+
result.bloatedInterfaces++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (exp.type === "interface") {
|
|
75
|
+
result.totalInterfaces++;
|
|
76
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
77
|
+
if (total > 5) {
|
|
78
|
+
result.bloatedInterfaces++;
|
|
79
|
+
}
|
|
159
80
|
}
|
|
160
81
|
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
161
84
|
}
|
|
162
|
-
ast.body.forEach(visit);
|
|
163
85
|
return result;
|
|
164
86
|
}
|
|
165
87
|
function detectTestFramework(rootDir) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
88
|
+
const manifests = [
|
|
89
|
+
{
|
|
90
|
+
file: "package.json",
|
|
91
|
+
deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
|
|
92
|
+
},
|
|
93
|
+
{ file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
|
|
94
|
+
{ file: "pyproject.toml", deps: ["pytest"] },
|
|
95
|
+
{ file: "pom.xml", deps: ["junit", "testng"] },
|
|
96
|
+
{ file: "build.gradle", deps: ["junit", "testng"] },
|
|
97
|
+
{ file: "go.mod", deps: ["testing"] }
|
|
98
|
+
// go testing is built-in
|
|
99
|
+
];
|
|
100
|
+
for (const m of manifests) {
|
|
101
|
+
const p = (0, import_path.join)(rootDir, m.file);
|
|
102
|
+
if ((0, import_fs.existsSync)(p)) {
|
|
103
|
+
if (m.file === "go.mod") return true;
|
|
104
|
+
try {
|
|
105
|
+
const content = (0, import_fs.readFileSync)(p, "utf-8");
|
|
106
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
187
110
|
}
|
|
111
|
+
return false;
|
|
188
112
|
}
|
|
189
113
|
var TEST_PATTERNS = [
|
|
190
114
|
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
115
|
+
/_test\.go$/,
|
|
116
|
+
/test_.*\.py$/,
|
|
117
|
+
/.*_test\.py$/,
|
|
118
|
+
/.*Test\.java$/,
|
|
119
|
+
/.*Tests\.cs$/,
|
|
191
120
|
/__tests__\//,
|
|
192
121
|
/\/tests?\//,
|
|
193
122
|
/\/e2e\//,
|
|
@@ -201,7 +130,7 @@ function isTestFile(filePath, extra) {
|
|
|
201
130
|
async function analyzeTestability(options) {
|
|
202
131
|
const allFiles = await (0, import_core.scanFiles)({
|
|
203
132
|
...options,
|
|
204
|
-
include: options.include || ["**/*.{ts,tsx,js,jsx}"],
|
|
133
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
205
134
|
includeTests: true
|
|
206
135
|
});
|
|
207
136
|
const sourceFiles = allFiles.filter(
|
|
@@ -227,7 +156,7 @@ async function analyzeTestability(options) {
|
|
|
227
156
|
"analyzing files",
|
|
228
157
|
options.onProgress
|
|
229
158
|
);
|
|
230
|
-
const a = analyzeFileTestability(f);
|
|
159
|
+
const a = await analyzeFileTestability(f);
|
|
231
160
|
for (const key of Object.keys(aggregated)) {
|
|
232
161
|
aggregated[key] += a[key];
|
|
233
162
|
}
|
|
@@ -253,9 +182,9 @@ async function analyzeTestability(options) {
|
|
|
253
182
|
type: import_core.IssueType.LowTestability,
|
|
254
183
|
dimension: "framework",
|
|
255
184
|
severity: import_core.Severity.Critical,
|
|
256
|
-
message: "No testing framework detected
|
|
185
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
257
186
|
location: { file: options.rootDir, line: 0 },
|
|
258
|
-
suggestion: "Add Jest,
|
|
187
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
259
188
|
});
|
|
260
189
|
}
|
|
261
190
|
if (actualRatio < minCoverage) {
|
|
@@ -274,19 +203,9 @@ async function analyzeTestability(options) {
|
|
|
274
203
|
type: import_core.IssueType.LowTestability,
|
|
275
204
|
dimension: "purity",
|
|
276
205
|
severity: import_core.Severity.Major,
|
|
277
|
-
message: `Only ${indexResult.dimensions.purityScore}% of functions
|
|
278
|
-
location: { file: options.rootDir, line: 0 },
|
|
279
|
-
suggestion: "Extract pure transformation logic from I/O and mutation code."
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
if (indexResult.dimensions.observabilityScore < 50) {
|
|
283
|
-
issues.push({
|
|
284
|
-
type: import_core.IssueType.LowTestability,
|
|
285
|
-
dimension: "observability",
|
|
286
|
-
severity: import_core.Severity.Major,
|
|
287
|
-
message: `Many functions mutate external state directly \u2014 outputs are invisible to unit tests.`,
|
|
206
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
288
207
|
location: { file: options.rootDir, line: 0 },
|
|
289
|
-
suggestion: "
|
|
208
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
290
209
|
});
|
|
291
210
|
}
|
|
292
211
|
return {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/testability",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "Measures how safely and verifiably AI-generated changes can be made to your codebase",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
42
|
"glob": "^13.0.0",
|
|
43
|
-
"@aiready/core": "0.21.
|
|
43
|
+
"@aiready/core": "0.21.18"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Testability analyzer.
|
|
3
|
-
*
|
|
4
|
-
* Walks the codebase and measures 5 structural dimensions that determine
|
|
5
|
-
* whether AI-generated changes can be safely verified:
|
|
6
|
-
* 1. Test file coverage ratio
|
|
7
|
-
* 2. Pure function prevalence
|
|
8
|
-
* 3. Dependency injection patterns
|
|
9
|
-
* 4. Interface focus (bloated interface detection)
|
|
10
|
-
* 5. Observability (return values vs. external state mutations)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
import {
|
|
14
2
|
scanFiles,
|
|
15
3
|
calculateTestabilityIndex,
|
|
16
4
|
Severity,
|
|
17
5
|
IssueType,
|
|
18
6
|
emitProgress,
|
|
7
|
+
getParser,
|
|
8
|
+
Language,
|
|
19
9
|
} from '@aiready/core';
|
|
20
10
|
import { readFileSync, existsSync } from 'fs';
|
|
21
11
|
import { join } from 'path';
|
|
22
|
-
import { parse } from '@typescript-eslint/typescript-estree';
|
|
23
|
-
import type { TSESTree } from '@typescript-eslint/types';
|
|
24
12
|
import type {
|
|
25
13
|
TestabilityOptions,
|
|
26
14
|
TestabilityIssue,
|
|
@@ -41,129 +29,7 @@ interface FileAnalysis {
|
|
|
41
29
|
externalStateMutations: number;
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
function
|
|
45
|
-
node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration
|
|
46
|
-
): number {
|
|
47
|
-
// Count method signatures
|
|
48
|
-
if (node.type === 'TSInterfaceDeclaration') {
|
|
49
|
-
return node.body.body.filter(
|
|
50
|
-
(m) => m.type === 'TSMethodSignature' || m.type === 'TSPropertySignature'
|
|
51
|
-
).length;
|
|
52
|
-
}
|
|
53
|
-
if (
|
|
54
|
-
node.type === 'TSTypeAliasDeclaration' &&
|
|
55
|
-
node.typeAnnotation.type === 'TSTypeLiteral'
|
|
56
|
-
) {
|
|
57
|
-
return node.typeAnnotation.members.length;
|
|
58
|
-
}
|
|
59
|
-
return 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function hasDependencyInjection(
|
|
63
|
-
node: TSESTree.ClassDeclaration | TSESTree.ClassExpression
|
|
64
|
-
): boolean {
|
|
65
|
-
// Look for a constructor with typed parameters (the most common DI pattern)
|
|
66
|
-
for (const member of node.body.body) {
|
|
67
|
-
if (
|
|
68
|
-
member.type === 'MethodDefinition' &&
|
|
69
|
-
member.key.type === 'Identifier' &&
|
|
70
|
-
member.key.name === 'constructor'
|
|
71
|
-
) {
|
|
72
|
-
const fn = member.value;
|
|
73
|
-
if (fn.params && fn.params.length > 0) {
|
|
74
|
-
// If constructor takes parameters that are typed class/interface references, that's DI
|
|
75
|
-
const typedParams = fn.params.filter((p) => {
|
|
76
|
-
const param = p as any;
|
|
77
|
-
return (
|
|
78
|
-
param.typeAnnotation != null ||
|
|
79
|
-
param.parameter?.typeAnnotation != null
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
if (typedParams.length > 0) return true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isPureFunction(
|
|
90
|
-
fn:
|
|
91
|
-
| TSESTree.FunctionDeclaration
|
|
92
|
-
| TSESTree.FunctionExpression
|
|
93
|
-
| TSESTree.ArrowFunctionExpression
|
|
94
|
-
): boolean {
|
|
95
|
-
let hasReturn = false;
|
|
96
|
-
let hasSideEffect = false;
|
|
97
|
-
|
|
98
|
-
function walk(node: TSESTree.Node) {
|
|
99
|
-
if (node.type === 'ReturnStatement' && node.argument) hasReturn = true;
|
|
100
|
-
if (
|
|
101
|
-
node.type === 'AssignmentExpression' &&
|
|
102
|
-
node.left.type === 'MemberExpression'
|
|
103
|
-
)
|
|
104
|
-
hasSideEffect = true;
|
|
105
|
-
// Calls to console, process, global objects
|
|
106
|
-
if (
|
|
107
|
-
node.type === 'CallExpression' &&
|
|
108
|
-
node.callee.type === 'MemberExpression' &&
|
|
109
|
-
node.callee.object.type === 'Identifier' &&
|
|
110
|
-
['console', 'process', 'window', 'document', 'fs'].includes(
|
|
111
|
-
node.callee.object.name
|
|
112
|
-
)
|
|
113
|
-
)
|
|
114
|
-
hasSideEffect = true;
|
|
115
|
-
|
|
116
|
-
// Recurse
|
|
117
|
-
for (const key of Object.keys(node)) {
|
|
118
|
-
if (key === 'parent') continue;
|
|
119
|
-
const child = (node as any)[key];
|
|
120
|
-
if (child && typeof child === 'object') {
|
|
121
|
-
if (Array.isArray(child)) {
|
|
122
|
-
child.forEach((c) => c?.type && walk(c));
|
|
123
|
-
} else if (child.type) {
|
|
124
|
-
walk(child);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (fn.body?.type === 'BlockStatement') {
|
|
131
|
-
fn.body.body.forEach((s) => walk(s));
|
|
132
|
-
} else if (fn.body) {
|
|
133
|
-
hasReturn = true; // arrow expression body
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return hasReturn && !hasSideEffect;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function hasExternalStateMutation(
|
|
140
|
-
fn:
|
|
141
|
-
| TSESTree.FunctionDeclaration
|
|
142
|
-
| TSESTree.FunctionExpression
|
|
143
|
-
| TSESTree.ArrowFunctionExpression
|
|
144
|
-
): boolean {
|
|
145
|
-
let found = false;
|
|
146
|
-
function walk(node: TSESTree.Node) {
|
|
147
|
-
if (found) return;
|
|
148
|
-
if (
|
|
149
|
-
node.type === 'AssignmentExpression' &&
|
|
150
|
-
node.left.type === 'MemberExpression'
|
|
151
|
-
)
|
|
152
|
-
found = true;
|
|
153
|
-
for (const key of Object.keys(node)) {
|
|
154
|
-
if (key === 'parent') continue;
|
|
155
|
-
const child = (node as any)[key];
|
|
156
|
-
if (child && typeof child === 'object') {
|
|
157
|
-
if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
|
|
158
|
-
else if (child.type) walk(child);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (fn.body?.type === 'BlockStatement') fn.body.body.forEach((s) => walk(s));
|
|
163
|
-
return found;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function analyzeFileTestability(filePath: string): FileAnalysis {
|
|
32
|
+
async function analyzeFileTestability(filePath: string): Promise<FileAnalysis> {
|
|
167
33
|
const result: FileAnalysis = {
|
|
168
34
|
pureFunctions: 0,
|
|
169
35
|
totalFunctions: 0,
|
|
@@ -174,6 +40,9 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
|
|
|
174
40
|
externalStateMutations: 0,
|
|
175
41
|
};
|
|
176
42
|
|
|
43
|
+
const parser = getParser(filePath);
|
|
44
|
+
if (!parser) return result;
|
|
45
|
+
|
|
177
46
|
let code: string;
|
|
178
47
|
try {
|
|
179
48
|
code = readFileSync(filePath, 'utf-8');
|
|
@@ -181,54 +50,43 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
|
|
|
181
50
|
return result;
|
|
182
51
|
}
|
|
183
52
|
|
|
184
|
-
let ast: TSESTree.Program;
|
|
185
53
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
function visit(node: TSESTree.Node) {
|
|
196
|
-
if (
|
|
197
|
-
node.type === 'FunctionDeclaration' ||
|
|
198
|
-
node.type === 'FunctionExpression' ||
|
|
199
|
-
node.type === 'ArrowFunctionExpression'
|
|
200
|
-
) {
|
|
201
|
-
result.totalFunctions++;
|
|
202
|
-
if (isPureFunction(node)) result.pureFunctions++;
|
|
203
|
-
if (hasExternalStateMutation(node)) result.externalStateMutations++;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
207
|
-
result.totalClasses++;
|
|
208
|
-
if (hasDependencyInjection(node)) result.injectionPatterns++;
|
|
209
|
-
}
|
|
54
|
+
await parser.initialize();
|
|
55
|
+
const parseResult = parser.parse(code, filePath);
|
|
56
|
+
|
|
57
|
+
for (const exp of parseResult.exports) {
|
|
58
|
+
if (exp.type === 'function') {
|
|
59
|
+
result.totalFunctions++;
|
|
60
|
+
if (exp.isPure) result.pureFunctions++;
|
|
61
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
62
|
+
}
|
|
210
63
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
64
|
+
if (exp.type === 'class') {
|
|
65
|
+
result.totalClasses++;
|
|
66
|
+
// Generalized DI heuristic: constructor/initializer with parameters
|
|
67
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
68
|
+
result.injectionPatterns++;
|
|
69
|
+
}
|
|
70
|
+
// Heuristic: bloated classes
|
|
71
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
72
|
+
if (total > 5) {
|
|
73
|
+
result.bloatedInterfaces++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
219
76
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
77
|
+
if (exp.type === 'interface') {
|
|
78
|
+
result.totalInterfaces++;
|
|
79
|
+
// Heuristic: interfaces with many methods/props are considered bloated
|
|
80
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
81
|
+
if (total > 5) {
|
|
82
|
+
result.bloatedInterfaces++;
|
|
83
|
+
}
|
|
227
84
|
}
|
|
228
85
|
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
229
88
|
}
|
|
230
89
|
|
|
231
|
-
ast.body.forEach(visit);
|
|
232
90
|
return result;
|
|
233
91
|
}
|
|
234
92
|
|
|
@@ -237,28 +95,30 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
|
|
|
237
95
|
// ---------------------------------------------------------------------------
|
|
238
96
|
|
|
239
97
|
function detectTestFramework(rootDir: string): boolean {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
'
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
98
|
+
// Check common manifest files
|
|
99
|
+
const manifests = [
|
|
100
|
+
{
|
|
101
|
+
file: 'package.json',
|
|
102
|
+
deps: ['jest', 'vitest', 'mocha', 'mocha', 'jasmine', 'ava', 'tap'],
|
|
103
|
+
},
|
|
104
|
+
{ file: 'requirements.txt', deps: ['pytest', 'unittest', 'nose'] },
|
|
105
|
+
{ file: 'pyproject.toml', deps: ['pytest'] },
|
|
106
|
+
{ file: 'pom.xml', deps: ['junit', 'testng'] },
|
|
107
|
+
{ file: 'build.gradle', deps: ['junit', 'testng'] },
|
|
108
|
+
{ file: 'go.mod', deps: ['testing'] }, // go testing is built-in
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const m of manifests) {
|
|
112
|
+
const p = join(rootDir, m.file);
|
|
113
|
+
if (existsSync(p)) {
|
|
114
|
+
if (m.file === 'go.mod') return true; // built-in
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync(p, 'utf-8');
|
|
117
|
+
if (m.deps.some((d) => content.includes(d))) return true;
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
261
120
|
}
|
|
121
|
+
return false;
|
|
262
122
|
}
|
|
263
123
|
|
|
264
124
|
// ---------------------------------------------------------------------------
|
|
@@ -267,6 +127,11 @@ function detectTestFramework(rootDir: string): boolean {
|
|
|
267
127
|
|
|
268
128
|
const TEST_PATTERNS = [
|
|
269
129
|
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
130
|
+
/_test\.go$/,
|
|
131
|
+
/test_.*\.py$/,
|
|
132
|
+
/.*_test\.py$/,
|
|
133
|
+
/.*Test\.java$/,
|
|
134
|
+
/.*Tests\.cs$/,
|
|
270
135
|
/__tests__\//,
|
|
271
136
|
/\/tests?\//,
|
|
272
137
|
/\/e2e\//,
|
|
@@ -285,7 +150,7 @@ export async function analyzeTestability(
|
|
|
285
150
|
// Use core scanFiles which respects .gitignore recursively
|
|
286
151
|
const allFiles = await scanFiles({
|
|
287
152
|
...options,
|
|
288
|
-
include: options.include || ['**/*.{ts,tsx,js,jsx}'],
|
|
153
|
+
include: options.include || ['**/*.{ts,tsx,js,jsx,py,java,cs,go}'],
|
|
289
154
|
includeTests: true,
|
|
290
155
|
});
|
|
291
156
|
|
|
@@ -315,7 +180,7 @@ export async function analyzeTestability(
|
|
|
315
180
|
options.onProgress
|
|
316
181
|
);
|
|
317
182
|
|
|
318
|
-
const a = analyzeFileTestability(f);
|
|
183
|
+
const a = await analyzeFileTestability(f);
|
|
319
184
|
for (const key of Object.keys(aggregated) as Array<keyof FileAnalysis>) {
|
|
320
185
|
aggregated[key] += a[key];
|
|
321
186
|
}
|
|
@@ -348,10 +213,10 @@ export async function analyzeTestability(
|
|
|
348
213
|
dimension: 'framework',
|
|
349
214
|
severity: Severity.Critical,
|
|
350
215
|
message:
|
|
351
|
-
'No testing framework detected
|
|
216
|
+
'No major testing framework detected — AI changes cannot be safely verified.',
|
|
352
217
|
location: { file: options.rootDir, line: 0 },
|
|
353
218
|
suggestion:
|
|
354
|
-
'Add Jest,
|
|
219
|
+
'Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification.',
|
|
355
220
|
});
|
|
356
221
|
}
|
|
357
222
|
|
|
@@ -373,21 +238,10 @@ export async function analyzeTestability(
|
|
|
373
238
|
type: IssueType.LowTestability,
|
|
374
239
|
dimension: 'purity',
|
|
375
240
|
severity: Severity.Major,
|
|
376
|
-
message: `Only ${indexResult.dimensions.purityScore}% of functions
|
|
241
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure — side-effectful code is harder for AI to verify safely.`,
|
|
377
242
|
location: { file: options.rootDir, line: 0 },
|
|
378
243
|
suggestion:
|
|
379
|
-
'
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (indexResult.dimensions.observabilityScore < 50) {
|
|
384
|
-
issues.push({
|
|
385
|
-
type: IssueType.LowTestability,
|
|
386
|
-
dimension: 'observability',
|
|
387
|
-
severity: Severity.Major,
|
|
388
|
-
message: `Many functions mutate external state directly — outputs are invisible to unit tests.`,
|
|
389
|
-
location: { file: options.rootDir, line: 0 },
|
|
390
|
-
suggestion: 'Prefer returning values over mutating shared state.',
|
|
244
|
+
'Refactor complex side-effectful logic into pure functions where possible.',
|
|
391
245
|
});
|
|
392
246
|
}
|
|
393
247
|
|