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.
@@ -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
+ }