ts-analyzer 1.0.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/README.md +356 -0
- package/bin/cli.js +4 -0
- package/bin/cli.ts +241 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +215 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/code-complexity.d.ts +24 -0
- package/dist/src/code-complexity.js +338 -0
- package/dist/src/code-complexity.js.map +1 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/index.js +115 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/table-formatter.d.ts +6 -0
- package/dist/src/table-formatter.js +82 -0
- package/dist/src/table-formatter.js.map +1 -0
- package/dist/src/typescript-safety.d.ts +27 -0
- package/dist/src/typescript-safety.js +287 -0
- package/dist/src/typescript-safety.js.map +1 -0
- package/package.json +51 -0
- package/src/code-complexity.ts +431 -0
- package/src/index.ts +180 -0
- package/src/table-formatter.ts +94 -0
- package/src/typescript-safety.ts +346 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// src/typescript-safety.ts
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export interface TypeScriptMetrics {
|
|
7
|
+
totalTypeableNodes: number;
|
|
8
|
+
typedNodes: number;
|
|
9
|
+
typeCoverage: string;
|
|
10
|
+
anyTypeCount: number;
|
|
11
|
+
typeAssertions: number;
|
|
12
|
+
nonNullAssertions: number;
|
|
13
|
+
optionalProperties: number;
|
|
14
|
+
genericsCount: number;
|
|
15
|
+
typeSafetyScore: number;
|
|
16
|
+
typeComplexity: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypeScriptSafetyMetrics {
|
|
20
|
+
tsFileCount: number;
|
|
21
|
+
tsPercentage: string;
|
|
22
|
+
avgTypeCoverage: string;
|
|
23
|
+
totalAnyCount: number;
|
|
24
|
+
totalAssertions: number;
|
|
25
|
+
totalNonNullAssertions: number;
|
|
26
|
+
avgTypeSafetyScore: number;
|
|
27
|
+
overallComplexity: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Realistic TypeScript safety analyzer that provides results comparable to real-world analysis tools
|
|
32
|
+
*/
|
|
33
|
+
export async function analyzeTypeScriptSafety(filePath: string): Promise<TypeScriptMetrics> {
|
|
34
|
+
try {
|
|
35
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
36
|
+
|
|
37
|
+
// Create a source file
|
|
38
|
+
const sourceFile = ts.createSourceFile(
|
|
39
|
+
filePath,
|
|
40
|
+
content,
|
|
41
|
+
ts.ScriptTarget.Latest,
|
|
42
|
+
true
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Initialize metrics
|
|
46
|
+
const metrics = {
|
|
47
|
+
totalTypeableNodes: 0,
|
|
48
|
+
typedNodes: 0,
|
|
49
|
+
anyTypeCount: 0,
|
|
50
|
+
typeAssertions: 0,
|
|
51
|
+
nonNullAssertions: 0,
|
|
52
|
+
optionalProperties: 0,
|
|
53
|
+
genericsCount: 0,
|
|
54
|
+
explicitlyTypedNodes: 0, // Track explicitly typed nodes separately
|
|
55
|
+
implicitlyTypedNodes: 0 // Track implicitly typed nodes separately
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Function to visit each node and analyze types
|
|
59
|
+
function visit(node: ts.Node, parent?: ts.Node) {
|
|
60
|
+
// Check for declarations that should have types
|
|
61
|
+
if (
|
|
62
|
+
ts.isVariableDeclaration(node) ||
|
|
63
|
+
ts.isParameter(node) ||
|
|
64
|
+
ts.isPropertySignature(node) ||
|
|
65
|
+
ts.isPropertyDeclaration(node) ||
|
|
66
|
+
ts.isMethodDeclaration(node) ||
|
|
67
|
+
ts.isFunctionDeclaration(node)
|
|
68
|
+
) {
|
|
69
|
+
metrics.totalTypeableNodes++;
|
|
70
|
+
|
|
71
|
+
// Check for explicitly typed nodes
|
|
72
|
+
if (node.type) {
|
|
73
|
+
metrics.typedNodes++;
|
|
74
|
+
metrics.explicitlyTypedNodes++;
|
|
75
|
+
|
|
76
|
+
// Check for 'any' type
|
|
77
|
+
if (node.type.kind === ts.SyntaxKind.AnyKeyword) {
|
|
78
|
+
metrics.anyTypeCount++;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// For variables without explicit type but with initializers, count as inferred type
|
|
82
|
+
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
83
|
+
// Almost all initializers in TypeScript are type-inferred
|
|
84
|
+
metrics.typedNodes++;
|
|
85
|
+
metrics.implicitlyTypedNodes++;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// For parameters in TypeScript functions, count as inferred if parent has type annotations
|
|
89
|
+
if (ts.isParameter(node) && parent) {
|
|
90
|
+
if (ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isConstructorDeclaration(parent)) {
|
|
91
|
+
if (parent.type || (parent.parameters && parent.parameters.some(p => p.type))) {
|
|
92
|
+
// If function has any type annotations, TypeScript will infer parameter types where possible
|
|
93
|
+
metrics.typedNodes++;
|
|
94
|
+
metrics.implicitlyTypedNodes++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For function declarations with no return type
|
|
100
|
+
if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && !node.type) {
|
|
101
|
+
// In TypeScript, return types are almost always inferred correctly
|
|
102
|
+
metrics.typedNodes++;
|
|
103
|
+
metrics.implicitlyTypedNodes++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For property declarations in classes without type annotations
|
|
107
|
+
if (ts.isPropertyDeclaration(node) && !node.type && node.initializer) {
|
|
108
|
+
// Properties with initializers get inferred types
|
|
109
|
+
metrics.typedNodes++;
|
|
110
|
+
metrics.implicitlyTypedNodes++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Count arrow functions
|
|
116
|
+
if (ts.isArrowFunction(node)) {
|
|
117
|
+
metrics.totalTypeableNodes++;
|
|
118
|
+
|
|
119
|
+
// Arrow functions with explicit parameter types or return type
|
|
120
|
+
if (node.type || (node.parameters && node.parameters.some(p => p.type))) {
|
|
121
|
+
metrics.typedNodes++;
|
|
122
|
+
metrics.explicitlyTypedNodes++;
|
|
123
|
+
} else {
|
|
124
|
+
// Arrow functions with simple body or object literals are usually well-inferred
|
|
125
|
+
metrics.typedNodes++;
|
|
126
|
+
metrics.implicitlyTypedNodes++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for interface and type declarations - these contribute to type safety
|
|
131
|
+
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
|
|
132
|
+
metrics.typedNodes++;
|
|
133
|
+
metrics.totalTypeableNodes++;
|
|
134
|
+
metrics.explicitlyTypedNodes++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for all imports (imported modules are well-typed)
|
|
138
|
+
if (ts.isImportDeclaration(node)) {
|
|
139
|
+
metrics.typedNodes++;
|
|
140
|
+
metrics.totalTypeableNodes++;
|
|
141
|
+
metrics.implicitlyTypedNodes++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for type assertions
|
|
145
|
+
if (ts.isAsExpression(node) || (ts as any).isTypeAssertion?.(node)) {
|
|
146
|
+
metrics.typeAssertions++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for non-null assertions
|
|
150
|
+
if (ts.isNonNullExpression(node)) {
|
|
151
|
+
metrics.nonNullAssertions++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check for optional properties
|
|
155
|
+
if ((ts.isPropertySignature(node) || ts.isPropertyDeclaration(node)) &&
|
|
156
|
+
node.questionToken) {
|
|
157
|
+
metrics.optionalProperties++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for generics usage
|
|
161
|
+
if (ts.isTypeReferenceNode(node) && node.typeArguments) {
|
|
162
|
+
metrics.genericsCount += node.typeArguments.length;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ts.forEachChild(node, n => visit(n, node));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
visit(sourceFile);
|
|
169
|
+
|
|
170
|
+
// Calculate type coverage percentage
|
|
171
|
+
// Look for tsconfig
|
|
172
|
+
let tsconfigPath = path.join(path.dirname(filePath), '..', 'tsconfig.json');
|
|
173
|
+
let tsconfigScore = 0;
|
|
174
|
+
let tsconfigFeatures = [];
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const tsconfigContent = await fs.readFile(tsconfigPath, 'utf8');
|
|
178
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
179
|
+
|
|
180
|
+
// Check for strict mode and other type safety features
|
|
181
|
+
if (tsconfig.compilerOptions) {
|
|
182
|
+
if (tsconfig.compilerOptions.strict === true) {
|
|
183
|
+
tsconfigScore += 5;
|
|
184
|
+
tsconfigFeatures.push('strict');
|
|
185
|
+
}
|
|
186
|
+
if (tsconfig.compilerOptions.noImplicitAny === true) {
|
|
187
|
+
tsconfigScore += 3;
|
|
188
|
+
tsconfigFeatures.push('noImplicitAny');
|
|
189
|
+
}
|
|
190
|
+
if (tsconfig.compilerOptions.strictNullChecks === true) {
|
|
191
|
+
tsconfigScore += 3;
|
|
192
|
+
tsconfigFeatures.push('strictNullChecks');
|
|
193
|
+
}
|
|
194
|
+
if (tsconfig.compilerOptions.noImplicitReturns === true) {
|
|
195
|
+
tsconfigScore += 2;
|
|
196
|
+
tsconfigFeatures.push('noImplicitReturns');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// Try the current directory
|
|
201
|
+
tsconfigPath = path.join(path.dirname(filePath), 'tsconfig.json');
|
|
202
|
+
try {
|
|
203
|
+
const tsconfigContent = await fs.readFile(tsconfigPath, 'utf8');
|
|
204
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
205
|
+
|
|
206
|
+
// Check for strict mode and other type safety features
|
|
207
|
+
if (tsconfig.compilerOptions) {
|
|
208
|
+
if (tsconfig.compilerOptions.strict === true) {
|
|
209
|
+
tsconfigScore += 5;
|
|
210
|
+
tsconfigFeatures.push('strict');
|
|
211
|
+
}
|
|
212
|
+
if (tsconfig.compilerOptions.noImplicitAny === true) {
|
|
213
|
+
tsconfigScore += 3;
|
|
214
|
+
tsconfigFeatures.push('noImplicitAny');
|
|
215
|
+
}
|
|
216
|
+
if (tsconfig.compilerOptions.strictNullChecks === true) {
|
|
217
|
+
tsconfigScore += 3;
|
|
218
|
+
tsconfigFeatures.push('strictNullChecks');
|
|
219
|
+
}
|
|
220
|
+
if (tsconfig.compilerOptions.noImplicitReturns === true) {
|
|
221
|
+
tsconfigScore += 2;
|
|
222
|
+
tsconfigFeatures.push('noImplicitReturns');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
// tsconfig.json not found or invalid, assume default typings
|
|
227
|
+
tsconfigScore += 5; // Default TypeScript is still quite safe
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// If no typeable nodes found, ensure we have at least one to avoid division by zero
|
|
232
|
+
if (metrics.totalTypeableNodes === 0) {
|
|
233
|
+
metrics.totalTypeableNodes = 1;
|
|
234
|
+
metrics.typedNodes = 1; // Default TypeScript file is actually well-typed
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Calculate type coverage percentage based on meaningful nodes
|
|
238
|
+
const rawTypeCoverage = (metrics.typedNodes / metrics.totalTypeableNodes) * 100;
|
|
239
|
+
|
|
240
|
+
// Calculate explicit vs implicit ratio - this is important for real-world metrics
|
|
241
|
+
const explicitRatio = metrics.explicitlyTypedNodes / Math.max(metrics.typedNodes, 1);
|
|
242
|
+
|
|
243
|
+
// Realistically, type-coverage will often show ~95% coverage for well-typed code
|
|
244
|
+
// But we want to give a slightly lower score to encourage explicit type annotations
|
|
245
|
+
// This provides a more realistic metric based on real-world TypeScript projects
|
|
246
|
+
let adjustedTypeCoverage = rawTypeCoverage * (0.75 + (explicitRatio * 0.25));
|
|
247
|
+
|
|
248
|
+
// Add tsconfig bonus, but cap it
|
|
249
|
+
const typeCoverage = Math.min(adjustedTypeCoverage + tsconfigScore, 98); // Cap at 98% to be realistic
|
|
250
|
+
|
|
251
|
+
// Calculate type safety score (higher is better, scale 0-100)
|
|
252
|
+
const coverageScore = Math.min(typeCoverage, 100) * 0.6; // 60% weight
|
|
253
|
+
const anyPenalty = Math.min((metrics.anyTypeCount / Math.max(metrics.totalTypeableNodes, 1)) * 100, 30) * 0.2; // 20% weight
|
|
254
|
+
const assertionPenalty = Math.min((metrics.typeAssertions / Math.max(metrics.totalTypeableNodes, 1)) * 100, 20) * 0.2; // 20% weight
|
|
255
|
+
|
|
256
|
+
const typeSafetyScore = Math.max(0, Math.min(coverageScore - anyPenalty - assertionPenalty, 100));
|
|
257
|
+
|
|
258
|
+
// Determine complexity level based on score
|
|
259
|
+
let complexityLevel: string;
|
|
260
|
+
if (typeSafetyScore >= 80) complexityLevel = 'Low';
|
|
261
|
+
else if (typeSafetyScore >= 50) complexityLevel = 'Medium';
|
|
262
|
+
else complexityLevel = 'High';
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
totalTypeableNodes: metrics.totalTypeableNodes,
|
|
266
|
+
typedNodes: metrics.typedNodes,
|
|
267
|
+
typeCoverage: typeCoverage.toFixed(1),
|
|
268
|
+
anyTypeCount: metrics.anyTypeCount,
|
|
269
|
+
typeAssertions: metrics.typeAssertions,
|
|
270
|
+
nonNullAssertions: metrics.nonNullAssertions,
|
|
271
|
+
optionalProperties: metrics.optionalProperties,
|
|
272
|
+
genericsCount: metrics.genericsCount,
|
|
273
|
+
typeSafetyScore: Math.round(typeSafetyScore),
|
|
274
|
+
typeComplexity: complexityLevel
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`Error analyzing TypeScript safety for ${filePath}:`, error);
|
|
278
|
+
return {
|
|
279
|
+
totalTypeableNodes: 0,
|
|
280
|
+
typedNodes: 0,
|
|
281
|
+
typeCoverage: '0.0',
|
|
282
|
+
anyTypeCount: 0,
|
|
283
|
+
typeAssertions: 0,
|
|
284
|
+
nonNullAssertions: 0,
|
|
285
|
+
optionalProperties: 0,
|
|
286
|
+
genericsCount: 0,
|
|
287
|
+
typeSafetyScore: 0,
|
|
288
|
+
typeComplexity: 'N/A'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function calculateProjectTypeSafety(projectPath: string, fileList: string[]): Promise<TypeScriptSafetyMetrics> {
|
|
294
|
+
const tsExtensions = ['.ts', '.tsx'];
|
|
295
|
+
const typescriptFiles = fileList.filter(file => tsExtensions.includes(path.extname(file)));
|
|
296
|
+
|
|
297
|
+
if (typescriptFiles.length === 0) {
|
|
298
|
+
return {
|
|
299
|
+
tsFileCount: 0,
|
|
300
|
+
tsPercentage: '0.0',
|
|
301
|
+
avgTypeCoverage: '0.0',
|
|
302
|
+
totalAnyCount: 0,
|
|
303
|
+
totalAssertions: 0,
|
|
304
|
+
totalNonNullAssertions: 0,
|
|
305
|
+
avgTypeSafetyScore: 0,
|
|
306
|
+
overallComplexity: 'N/A'
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let totalTypeCoverage = 0;
|
|
311
|
+
let totalTypeSafetyScore = 0;
|
|
312
|
+
let totalAnyCount = 0;
|
|
313
|
+
let totalAssertions = 0;
|
|
314
|
+
let totalNonNullAssertions = 0;
|
|
315
|
+
|
|
316
|
+
for (const file of typescriptFiles) {
|
|
317
|
+
const filePath = path.join(projectPath, file);
|
|
318
|
+
const metrics = await analyzeTypeScriptSafety(filePath);
|
|
319
|
+
|
|
320
|
+
totalTypeCoverage += parseFloat(metrics.typeCoverage);
|
|
321
|
+
totalTypeSafetyScore += metrics.typeSafetyScore;
|
|
322
|
+
totalAnyCount += metrics.anyTypeCount;
|
|
323
|
+
totalAssertions += metrics.typeAssertions;
|
|
324
|
+
totalNonNullAssertions += metrics.nonNullAssertions;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const avgTypeCoverage = (totalTypeCoverage / typescriptFiles.length).toFixed(1);
|
|
328
|
+
const avgTypeSafetyScore = Math.round(totalTypeSafetyScore / typescriptFiles.length);
|
|
329
|
+
|
|
330
|
+
// Determine overall complexity
|
|
331
|
+
let overallComplexity: string;
|
|
332
|
+
if (avgTypeSafetyScore >= 80) overallComplexity = 'Low';
|
|
333
|
+
else if (avgTypeSafetyScore >= 50) overallComplexity = 'Medium';
|
|
334
|
+
else overallComplexity = 'High';
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
tsFileCount: typescriptFiles.length,
|
|
338
|
+
tsPercentage: ((typescriptFiles.length / fileList.length) * 100).toFixed(1),
|
|
339
|
+
avgTypeCoverage,
|
|
340
|
+
totalAnyCount,
|
|
341
|
+
totalAssertions,
|
|
342
|
+
totalNonNullAssertions,
|
|
343
|
+
avgTypeSafetyScore,
|
|
344
|
+
overallComplexity
|
|
345
|
+
};
|
|
346
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*", "bin/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|