ts-unused 1.0.0 → 1.0.2
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 +67 -2
- package/dist/analyzeFunctionReturnTypes.d.ts +6 -0
- package/dist/analyzeInterfaces.d.ts +3 -0
- package/dist/analyzeProject.d.ts +2 -0
- package/dist/analyzeTypeAliases.d.ts +5 -0
- package/dist/checkExportUsage.d.ts +3 -0
- package/dist/checkGitStatus.d.ts +7 -0
- package/dist/cli.js +477 -64
- package/dist/extractTodoComment.d.ts +6 -0
- package/dist/findNeverReturnedTypes.d.ts +3 -0
- package/dist/findStructurallyEquivalentProperties.d.ts +12 -0
- package/dist/findUnusedExports.d.ts +3 -0
- package/dist/findUnusedProperties.d.ts +3 -0
- package/dist/fixProject.d.ts +13 -0
- package/dist/formatResults.d.ts +2 -0
- package/dist/hasNoCheck.d.ts +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1251 -0
- package/dist/isPropertyUnused.d.ts +7 -0
- package/dist/isTestFile.d.ts +2 -0
- package/dist/types.d.ts +40 -0
- package/package.json +15 -4
package/dist/cli.js
CHANGED
|
@@ -2,17 +2,240 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import fs2 from "node:fs";
|
|
5
|
-
import
|
|
5
|
+
import path12 from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/analyzeProject.ts
|
|
8
|
-
import
|
|
8
|
+
import path8 from "node:path";
|
|
9
9
|
import { Project } from "ts-morph";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/findNeverReturnedTypes.ts
|
|
12
12
|
import path2 from "node:path";
|
|
13
|
+
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
13
14
|
|
|
14
|
-
// src/
|
|
15
|
+
// src/analyzeFunctionReturnTypes.ts
|
|
15
16
|
import path from "node:path";
|
|
17
|
+
import { SyntaxKind } from "ts-morph";
|
|
18
|
+
function analyzeFunctionReturnTypes(func, sourceFile, tsConfigDir) {
|
|
19
|
+
const results = [];
|
|
20
|
+
const functionName = func.getName();
|
|
21
|
+
if (!functionName) {
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
const returnTypeNode = func.getReturnTypeNode();
|
|
25
|
+
if (!returnTypeNode) {
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
const returnType = returnTypeNode.getType();
|
|
29
|
+
const unwrappedPromise = unwrapPromiseType(returnType);
|
|
30
|
+
const typeToCheck = unwrappedPromise || returnType;
|
|
31
|
+
if (!typeToCheck.isUnion()) {
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
const unionTypes = typeToCheck.getUnionTypes();
|
|
35
|
+
if (unionTypes.length < 2) {
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
const returnStatements = func.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
39
|
+
const returnedTypes = [];
|
|
40
|
+
for (const returnStmt of returnStatements) {
|
|
41
|
+
const expression = returnStmt.getExpression();
|
|
42
|
+
if (expression) {
|
|
43
|
+
const exprType = expression.getType();
|
|
44
|
+
const unwrapped = unwrapPromiseType(exprType);
|
|
45
|
+
returnedTypes.push(unwrapped || exprType);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (returnedTypes.length === 0) {
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
for (const returnedType of returnedTypes) {
|
|
52
|
+
if (returnedType.isAssignableTo(typeToCheck)) {
|
|
53
|
+
let assignableToAnyBranch = false;
|
|
54
|
+
for (const unionBranch of unionTypes) {
|
|
55
|
+
if (returnedType.isAssignableTo(unionBranch)) {
|
|
56
|
+
assignableToAnyBranch = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!assignableToAnyBranch) {
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const enumGroups = new Map;
|
|
66
|
+
const enumsWithReturnedValues = new Set;
|
|
67
|
+
for (const unionBranch of unionTypes) {
|
|
68
|
+
if (unionBranch.isEnumLiteral()) {
|
|
69
|
+
const enumName = getEnumNameFromLiteral(unionBranch);
|
|
70
|
+
if (enumName) {
|
|
71
|
+
if (!enumGroups.has(enumName)) {
|
|
72
|
+
enumGroups.set(enumName, []);
|
|
73
|
+
}
|
|
74
|
+
enumGroups.get(enumName)?.push(unionBranch);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const [enumName, enumBranches] of enumGroups.entries()) {
|
|
79
|
+
for (const returnedType of returnedTypes) {
|
|
80
|
+
for (const enumBranch of enumBranches) {
|
|
81
|
+
if (isTypeAssignableTo(returnedType, enumBranch)) {
|
|
82
|
+
enumsWithReturnedValues.add(enumName);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (enumsWithReturnedValues.has(enumName)) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const branchMap = new Map;
|
|
92
|
+
for (const unionBranch of unionTypes) {
|
|
93
|
+
const displayName = getTypeDisplayName(unionBranch);
|
|
94
|
+
if (!branchMap.has(displayName)) {
|
|
95
|
+
branchMap.set(displayName, unionBranch);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const [displayName, unionBranch] of branchMap.entries()) {
|
|
99
|
+
if (unionBranch.isEnumLiteral()) {
|
|
100
|
+
const enumName = getEnumNameFromLiteral(unionBranch);
|
|
101
|
+
if (enumName && enumsWithReturnedValues.has(enumName)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let isReturned = false;
|
|
106
|
+
for (const returnedType of returnedTypes) {
|
|
107
|
+
if (isTypeAssignableTo(returnedType, unionBranch)) {
|
|
108
|
+
isReturned = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!isReturned) {
|
|
113
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
114
|
+
const nameNode = func.getNameNode();
|
|
115
|
+
if (nameNode) {
|
|
116
|
+
const startPos = nameNode.getStart();
|
|
117
|
+
const lineStartPos = nameNode.getStartLinePos();
|
|
118
|
+
const character = startPos - lineStartPos + 1;
|
|
119
|
+
const endCharacter = character + functionName.length;
|
|
120
|
+
results.push({
|
|
121
|
+
filePath: relativePath,
|
|
122
|
+
functionName,
|
|
123
|
+
neverReturnedType: displayName,
|
|
124
|
+
line: nameNode.getStartLineNumber(),
|
|
125
|
+
character,
|
|
126
|
+
endCharacter,
|
|
127
|
+
severity: "error"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
function unwrapPromiseType(type) {
|
|
135
|
+
const symbol = type.getSymbol();
|
|
136
|
+
if (symbol?.getName() === "Promise") {
|
|
137
|
+
const typeArgs = type.getTypeArguments();
|
|
138
|
+
if (typeArgs.length > 0 && typeArgs[0]) {
|
|
139
|
+
return typeArgs[0];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function isTypeAssignableTo(sourceType, targetType) {
|
|
145
|
+
if (sourceType.isAssignableTo(targetType)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
const sourceText = sourceType.getText();
|
|
149
|
+
const targetText = targetType.getText();
|
|
150
|
+
const isBooleanLiteral = (text) => text === "true" || text === "false";
|
|
151
|
+
if (isBooleanLiteral(sourceText) && isBooleanLiteral(targetText)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function getEnumNameFromLiteral(type) {
|
|
157
|
+
if (!type.isEnumLiteral()) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const typeText = type.getText();
|
|
161
|
+
const cleanedText = typeText.replace(/import\([^)]+\)\./g, "");
|
|
162
|
+
const lastDotIndex = cleanedText.lastIndexOf(".");
|
|
163
|
+
if (lastDotIndex > 0) {
|
|
164
|
+
return cleanedText.substring(0, lastDotIndex);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function getTypeDisplayName(type) {
|
|
169
|
+
const symbol = type.getSymbol();
|
|
170
|
+
if (symbol) {
|
|
171
|
+
const name = symbol.getName();
|
|
172
|
+
if (name && name !== "__type") {
|
|
173
|
+
return name;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let typeText = type.getText();
|
|
177
|
+
typeText = typeText.replace(/import\([^)]+\)\./g, "");
|
|
178
|
+
if (typeText === "string")
|
|
179
|
+
return "string";
|
|
180
|
+
if (typeText === "number")
|
|
181
|
+
return "number";
|
|
182
|
+
if (typeText === "boolean")
|
|
183
|
+
return "boolean";
|
|
184
|
+
if (typeText === "true" || typeText === "false")
|
|
185
|
+
return "boolean";
|
|
186
|
+
if (typeText === "null")
|
|
187
|
+
return "null";
|
|
188
|
+
if (typeText === "undefined")
|
|
189
|
+
return "undefined";
|
|
190
|
+
const MAX_TYPE_LENGTH = 100;
|
|
191
|
+
if (typeText.length > MAX_TYPE_LENGTH) {
|
|
192
|
+
return `${typeText.substring(0, MAX_TYPE_LENGTH)}...`;
|
|
193
|
+
}
|
|
194
|
+
return typeText;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/hasNoCheck.ts
|
|
198
|
+
function hasNoCheck(sourceFile) {
|
|
199
|
+
const fullText = sourceFile.getFullText();
|
|
200
|
+
const firstLine = fullText.split(`
|
|
201
|
+
`)[0];
|
|
202
|
+
if (!firstLine) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return firstLine.trim() === "// @ts-nocheck";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/findNeverReturnedTypes.ts
|
|
209
|
+
function findNeverReturnedTypes(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
212
|
+
if (isTestFile(sourceFile)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (hasNoCheck(sourceFile)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (onProgress) {
|
|
222
|
+
const relativePath = path2.relative(tsConfigDir, sourceFile.getFilePath());
|
|
223
|
+
onProgress(relativePath);
|
|
224
|
+
}
|
|
225
|
+
const functions = sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionDeclaration);
|
|
226
|
+
for (const func of functions) {
|
|
227
|
+
const funcResults = analyzeFunctionReturnTypes(func, sourceFile, tsConfigDir);
|
|
228
|
+
results.push(...funcResults);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/findUnusedExports.ts
|
|
235
|
+
import path4 from "node:path";
|
|
236
|
+
|
|
237
|
+
// src/checkExportUsage.ts
|
|
238
|
+
import path3 from "node:path";
|
|
16
239
|
import { Node } from "ts-morph";
|
|
17
240
|
function getExportKind(declaration) {
|
|
18
241
|
if (Node.isFunctionDeclaration(declaration)) {
|
|
@@ -84,7 +307,7 @@ function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isT
|
|
|
84
307
|
return null;
|
|
85
308
|
}
|
|
86
309
|
const kind = getExportKind(firstDeclaration);
|
|
87
|
-
const relativePath =
|
|
310
|
+
const relativePath = path3.relative(tsConfigDir, sourceFile.getFilePath());
|
|
88
311
|
const severity = onlyUsedInTests ? "info" : "error";
|
|
89
312
|
const nameNode = getNameNode(firstDeclaration);
|
|
90
313
|
const positionNode = nameNode || firstDeclaration;
|
|
@@ -105,17 +328,6 @@ function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isT
|
|
|
105
328
|
return result;
|
|
106
329
|
}
|
|
107
330
|
|
|
108
|
-
// src/hasNoCheck.ts
|
|
109
|
-
function hasNoCheck(sourceFile) {
|
|
110
|
-
const fullText = sourceFile.getFullText();
|
|
111
|
-
const firstLine = fullText.split(`
|
|
112
|
-
`)[0];
|
|
113
|
-
if (!firstLine) {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
return firstLine.trim() === "// @ts-nocheck";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
331
|
// src/findUnusedExports.ts
|
|
120
332
|
function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
121
333
|
const results = [];
|
|
@@ -130,7 +342,7 @@ function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetF
|
|
|
130
342
|
continue;
|
|
131
343
|
}
|
|
132
344
|
if (onProgress) {
|
|
133
|
-
const relativePath =
|
|
345
|
+
const relativePath = path4.relative(tsConfigDir, sourceFile.getFilePath());
|
|
134
346
|
onProgress(relativePath);
|
|
135
347
|
}
|
|
136
348
|
const exports = sourceFile.getExportedDeclarations();
|
|
@@ -145,10 +357,10 @@ function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetF
|
|
|
145
357
|
}
|
|
146
358
|
|
|
147
359
|
// src/findUnusedProperties.ts
|
|
148
|
-
import
|
|
360
|
+
import path7 from "node:path";
|
|
149
361
|
|
|
150
362
|
// src/analyzeInterfaces.ts
|
|
151
|
-
import
|
|
363
|
+
import path5 from "node:path";
|
|
152
364
|
|
|
153
365
|
// src/extractTodoComment.ts
|
|
154
366
|
function extractTodoComment(prop) {
|
|
@@ -174,7 +386,7 @@ function extractTodoComment(prop) {
|
|
|
174
386
|
}
|
|
175
387
|
|
|
176
388
|
// src/findStructurallyEquivalentProperties.ts
|
|
177
|
-
import { SyntaxKind } from "ts-morph";
|
|
389
|
+
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
178
390
|
function checkInterfaceProperties(iface, propName, propType, originalProp, equivalentProps) {
|
|
179
391
|
const properties = iface.getProperties();
|
|
180
392
|
for (const p of properties) {
|
|
@@ -191,15 +403,15 @@ function checkInterfaceProperties(iface, propName, propType, originalProp, equiv
|
|
|
191
403
|
}
|
|
192
404
|
function checkTypeAliasProperties(typeAlias, propName, propType, originalProp, equivalentProps) {
|
|
193
405
|
const typeNode = typeAlias.getTypeNode();
|
|
194
|
-
if (!typeNode || typeNode.getKind() !==
|
|
406
|
+
if (!typeNode || typeNode.getKind() !== SyntaxKind3.TypeLiteral) {
|
|
195
407
|
return;
|
|
196
408
|
}
|
|
197
|
-
const properties = typeNode.getChildren().filter((child) => child.getKind() ===
|
|
409
|
+
const properties = typeNode.getChildren().filter((child) => child.getKind() === SyntaxKind3.PropertySignature);
|
|
198
410
|
for (const p of properties) {
|
|
199
411
|
if (p === originalProp) {
|
|
200
412
|
continue;
|
|
201
413
|
}
|
|
202
|
-
if (p.getKind() ===
|
|
414
|
+
if (p.getKind() === SyntaxKind3.PropertySignature && p.getName() === propName) {
|
|
203
415
|
const pType = p.getType().getText();
|
|
204
416
|
if (pType === propType) {
|
|
205
417
|
equivalentProps.push(p);
|
|
@@ -278,7 +490,7 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
|
|
|
278
490
|
for (const prop of iface.getProperties()) {
|
|
279
491
|
const usage = isPropertyUnused(prop, isTestFile, project);
|
|
280
492
|
if (usage.isUnusedOrTestOnly) {
|
|
281
|
-
const relativePath =
|
|
493
|
+
const relativePath = path5.relative(tsConfigDir, sourceFile.getFilePath());
|
|
282
494
|
const todoComment = extractTodoComment(prop);
|
|
283
495
|
let severity = "error";
|
|
284
496
|
if (todoComment) {
|
|
@@ -309,7 +521,7 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
|
|
|
309
521
|
}
|
|
310
522
|
|
|
311
523
|
// src/analyzeTypeAliases.ts
|
|
312
|
-
import
|
|
524
|
+
import path6 from "node:path";
|
|
313
525
|
import { Node as Node2 } from "ts-morph";
|
|
314
526
|
function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
315
527
|
if (!Node2.isPropertySignature(member)) {
|
|
@@ -319,7 +531,7 @@ function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isT
|
|
|
319
531
|
if (!usage.isUnusedOrTestOnly) {
|
|
320
532
|
return;
|
|
321
533
|
}
|
|
322
|
-
const relativePath =
|
|
534
|
+
const relativePath = path6.relative(tsConfigDir, sourceFile.getFilePath());
|
|
323
535
|
const todoComment = extractTodoComment(member);
|
|
324
536
|
let severity = "error";
|
|
325
537
|
if (todoComment) {
|
|
@@ -379,7 +591,7 @@ function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targ
|
|
|
379
591
|
continue;
|
|
380
592
|
}
|
|
381
593
|
if (onProgress) {
|
|
382
|
-
const relativePath =
|
|
594
|
+
const relativePath = path7.relative(tsConfigDir, sourceFile.getFilePath());
|
|
383
595
|
onProgress(relativePath);
|
|
384
596
|
}
|
|
385
597
|
analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project);
|
|
@@ -400,7 +612,7 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
|
|
|
400
612
|
const project = new Project({
|
|
401
613
|
tsConfigFilePath: tsConfigPath
|
|
402
614
|
});
|
|
403
|
-
const tsConfigDir =
|
|
615
|
+
const tsConfigDir = path8.dirname(tsConfigPath);
|
|
404
616
|
const allSourceFiles = project.getSourceFiles();
|
|
405
617
|
const filesToAnalyze = allSourceFiles.filter((sf) => {
|
|
406
618
|
if (isTestFile2(sf)) {
|
|
@@ -426,43 +638,50 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
|
|
|
426
638
|
} : undefined;
|
|
427
639
|
const unusedExports = findUnusedExports(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
|
|
428
640
|
const unusedProperties = findUnusedProperties(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
|
|
641
|
+
const neverReturnedTypes = findNeverReturnedTypes(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
|
|
429
642
|
const unusedFiles = [];
|
|
430
643
|
const fileExportCounts = new Map;
|
|
431
644
|
for (const sourceFile of filesToAnalyze) {
|
|
432
|
-
const filePath =
|
|
645
|
+
const filePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
|
|
433
646
|
const exports = sourceFile.getExportedDeclarations();
|
|
434
647
|
const totalExports = exports.size;
|
|
435
648
|
if (totalExports > 0) {
|
|
436
|
-
fileExportCounts.set(filePath, { total: totalExports, unused: 0 });
|
|
649
|
+
fileExportCounts.set(filePath, { total: totalExports, unused: 0, testOnly: 0 });
|
|
437
650
|
}
|
|
438
651
|
}
|
|
439
652
|
for (const unusedExport of unusedExports) {
|
|
440
653
|
const counts = fileExportCounts.get(unusedExport.filePath);
|
|
441
654
|
if (counts) {
|
|
442
655
|
counts.unused++;
|
|
656
|
+
if (unusedExport.onlyUsedInTests) {
|
|
657
|
+
counts.testOnly++;
|
|
658
|
+
}
|
|
443
659
|
}
|
|
444
660
|
}
|
|
445
661
|
for (const [filePath, counts] of fileExportCounts.entries()) {
|
|
446
|
-
|
|
662
|
+
const allExportsUnused = counts.total > 0 && counts.unused === counts.total;
|
|
663
|
+
const hasAnyTestOnlyExports = counts.testOnly > 0;
|
|
664
|
+
if (allExportsUnused && !hasAnyTestOnlyExports) {
|
|
447
665
|
unusedFiles.push(filePath);
|
|
448
666
|
}
|
|
449
667
|
}
|
|
450
668
|
const results = {
|
|
451
669
|
unusedExports,
|
|
452
670
|
unusedProperties,
|
|
453
|
-
unusedFiles
|
|
671
|
+
unusedFiles,
|
|
672
|
+
neverReturnedTypes
|
|
454
673
|
};
|
|
455
674
|
return results;
|
|
456
675
|
}
|
|
457
676
|
|
|
458
677
|
// src/fixProject.ts
|
|
459
678
|
import fs from "node:fs";
|
|
460
|
-
import
|
|
461
|
-
import { Project as Project2, SyntaxKind as
|
|
679
|
+
import path10 from "node:path";
|
|
680
|
+
import { Project as Project2, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
462
681
|
|
|
463
682
|
// src/checkGitStatus.ts
|
|
464
683
|
import { execSync } from "node:child_process";
|
|
465
|
-
import
|
|
684
|
+
import path9 from "node:path";
|
|
466
685
|
function checkGitStatus(workingDir) {
|
|
467
686
|
const changedFiles = new Set;
|
|
468
687
|
try {
|
|
@@ -484,7 +703,7 @@ function checkGitStatus(workingDir) {
|
|
|
484
703
|
const filename = line.slice(3).trim();
|
|
485
704
|
const actualFilename = filename.includes(" -> ") ? filename.split(" -> ")[1] : filename;
|
|
486
705
|
if (actualFilename) {
|
|
487
|
-
const absolutePath =
|
|
706
|
+
const absolutePath = path9.resolve(gitRoot, actualFilename);
|
|
488
707
|
changedFiles.add(absolutePath);
|
|
489
708
|
}
|
|
490
709
|
}
|
|
@@ -499,19 +718,20 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
|
499
718
|
const results = {
|
|
500
719
|
fixedExports: 0,
|
|
501
720
|
fixedProperties: 0,
|
|
721
|
+
fixedNeverReturnedTypes: 0,
|
|
502
722
|
deletedFiles: 0,
|
|
503
723
|
skippedFiles: [],
|
|
504
724
|
errors: []
|
|
505
725
|
};
|
|
506
726
|
const analysis = analyzeProject(tsConfigPath, undefined, undefined, isTestFile2);
|
|
507
|
-
const tsConfigDir =
|
|
727
|
+
const tsConfigDir = path10.dirname(path10.resolve(tsConfigPath));
|
|
508
728
|
const filesWithChanges = checkGitStatus(tsConfigDir);
|
|
509
729
|
const project = new Project2({
|
|
510
730
|
tsConfigFilePath: tsConfigPath
|
|
511
731
|
});
|
|
512
732
|
const deletedFiles = new Set;
|
|
513
733
|
for (const relativeFilePath of analysis.unusedFiles) {
|
|
514
|
-
const absoluteFilePath =
|
|
734
|
+
const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
|
|
515
735
|
if (filesWithChanges.has(absoluteFilePath)) {
|
|
516
736
|
onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
|
|
517
737
|
results.skippedFiles.push(relativeFilePath);
|
|
@@ -531,15 +751,71 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
|
531
751
|
if (deletedFiles.size > 0) {
|
|
532
752
|
cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results);
|
|
533
753
|
}
|
|
754
|
+
const neverReturnedByFile = new Map;
|
|
755
|
+
for (const neverReturned of analysis.neverReturnedTypes || []) {
|
|
756
|
+
if (neverReturned.severity !== "error") {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (!neverReturnedByFile.has(neverReturned.filePath)) {
|
|
760
|
+
neverReturnedByFile.set(neverReturned.filePath, []);
|
|
761
|
+
}
|
|
762
|
+
neverReturnedByFile.get(neverReturned.filePath)?.push(neverReturned);
|
|
763
|
+
}
|
|
764
|
+
const unusedExportNames = new Set;
|
|
765
|
+
for (const unusedExport of analysis.unusedExports) {
|
|
766
|
+
if (unusedExport.severity === "error") {
|
|
767
|
+
unusedExportNames.add(`${unusedExport.filePath}:${unusedExport.exportName}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const [relativeFilePath, neverReturnedItems] of neverReturnedByFile.entries()) {
|
|
771
|
+
const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
|
|
772
|
+
if (analysis.unusedFiles.includes(relativeFilePath)) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (filesWithChanges.has(absoluteFilePath)) {
|
|
776
|
+
if (!results.skippedFiles.includes(relativeFilePath)) {
|
|
777
|
+
onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
|
|
778
|
+
results.skippedFiles.push(relativeFilePath);
|
|
779
|
+
}
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
const sourceFile = project.getSourceFile(absoluteFilePath);
|
|
784
|
+
if (!sourceFile) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
onProgress?.(`Fixing: ${relativeFilePath}`);
|
|
788
|
+
if (neverReturnedItems) {
|
|
789
|
+
for (const neverReturned of neverReturnedItems) {
|
|
790
|
+
const exportKey = `${relativeFilePath}:${neverReturned.functionName}`;
|
|
791
|
+
if (unusedExportNames.has(exportKey)) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (removeNeverReturnedType(sourceFile, neverReturned.functionName, neverReturned.neverReturnedType)) {
|
|
795
|
+
onProgress?.(` ✓ Removed never-returned type '${neverReturned.neverReturnedType}' from ${neverReturned.functionName}`);
|
|
796
|
+
results.fixedNeverReturnedTypes++;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
sourceFile.saveSync();
|
|
801
|
+
} catch (error) {
|
|
802
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
803
|
+
results.errors.push({ file: relativeFilePath, error: errorMessage });
|
|
804
|
+
onProgress?.(`Error fixing ${relativeFilePath}: ${errorMessage}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
534
807
|
const exportsByFile = new Map;
|
|
535
808
|
for (const unusedExport of analysis.unusedExports) {
|
|
809
|
+
if (unusedExport.severity !== "error") {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
536
812
|
if (!exportsByFile.has(unusedExport.filePath)) {
|
|
537
813
|
exportsByFile.set(unusedExport.filePath, []);
|
|
538
814
|
}
|
|
539
815
|
exportsByFile.get(unusedExport.filePath)?.push(unusedExport);
|
|
540
816
|
}
|
|
541
817
|
for (const [relativeFilePath, exports] of exportsByFile.entries()) {
|
|
542
|
-
const absoluteFilePath =
|
|
818
|
+
const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
|
|
543
819
|
if (analysis.unusedFiles.includes(relativeFilePath)) {
|
|
544
820
|
continue;
|
|
545
821
|
}
|
|
@@ -553,7 +829,9 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
|
553
829
|
if (!sourceFile) {
|
|
554
830
|
continue;
|
|
555
831
|
}
|
|
556
|
-
|
|
832
|
+
if (!neverReturnedByFile.has(relativeFilePath)) {
|
|
833
|
+
onProgress?.(`Fixing: ${relativeFilePath}`);
|
|
834
|
+
}
|
|
557
835
|
for (const unusedExport of exports) {
|
|
558
836
|
if (removeExport(sourceFile, unusedExport.exportName)) {
|
|
559
837
|
onProgress?.(` ✓ Removed unused export: ${unusedExport.exportName}`);
|
|
@@ -569,13 +847,16 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
|
569
847
|
}
|
|
570
848
|
const propertiesByFile = new Map;
|
|
571
849
|
for (const unusedProperty of analysis.unusedProperties) {
|
|
850
|
+
if (unusedProperty.severity !== "error") {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
572
853
|
if (!propertiesByFile.has(unusedProperty.filePath)) {
|
|
573
854
|
propertiesByFile.set(unusedProperty.filePath, []);
|
|
574
855
|
}
|
|
575
856
|
propertiesByFile.get(unusedProperty.filePath)?.push(unusedProperty);
|
|
576
857
|
}
|
|
577
858
|
for (const [relativeFilePath, properties] of propertiesByFile.entries()) {
|
|
578
|
-
const absoluteFilePath =
|
|
859
|
+
const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
|
|
579
860
|
if (analysis.unusedFiles.includes(relativeFilePath)) {
|
|
580
861
|
continue;
|
|
581
862
|
}
|
|
@@ -591,7 +872,7 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
|
591
872
|
if (!sourceFile) {
|
|
592
873
|
continue;
|
|
593
874
|
}
|
|
594
|
-
if (!exportsByFile.has(relativeFilePath)) {
|
|
875
|
+
if (!exportsByFile.has(relativeFilePath) && !neverReturnedByFile.has(relativeFilePath)) {
|
|
595
876
|
onProgress?.(`Fixing: ${relativeFilePath}`);
|
|
596
877
|
}
|
|
597
878
|
for (const unusedProperty of properties) {
|
|
@@ -616,8 +897,8 @@ function removeExport(sourceFile, exportName) {
|
|
|
616
897
|
return false;
|
|
617
898
|
}
|
|
618
899
|
for (const declaration of declarations) {
|
|
619
|
-
if (declaration.getKind() ===
|
|
620
|
-
const variableStatement = declaration.getFirstAncestorByKind(
|
|
900
|
+
if (declaration.getKind() === SyntaxKind4.VariableDeclaration) {
|
|
901
|
+
const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind4.VariableStatement);
|
|
621
902
|
if (variableStatement) {
|
|
622
903
|
variableStatement.remove();
|
|
623
904
|
continue;
|
|
@@ -644,12 +925,12 @@ function removeProperty(sourceFile, typeName, propertyName) {
|
|
|
644
925
|
for (const typeAlias of typeAliases) {
|
|
645
926
|
if (typeAlias.getName() === typeName) {
|
|
646
927
|
const typeNode = typeAlias.getTypeNode();
|
|
647
|
-
if (typeNode && typeNode.getKind() ===
|
|
648
|
-
const typeLiteral = typeNode.asKindOrThrow(
|
|
928
|
+
if (typeNode && typeNode.getKind() === SyntaxKind4.TypeLiteral) {
|
|
929
|
+
const typeLiteral = typeNode.asKindOrThrow(SyntaxKind4.TypeLiteral);
|
|
649
930
|
const members = typeLiteral.getMembers();
|
|
650
931
|
for (const member of members) {
|
|
651
|
-
if (member.getKind() ===
|
|
652
|
-
const propSig = member.asKind(
|
|
932
|
+
if (member.getKind() === SyntaxKind4.PropertySignature) {
|
|
933
|
+
const propSig = member.asKind(SyntaxKind4.PropertySignature);
|
|
653
934
|
if (propSig?.getName() === propertyName) {
|
|
654
935
|
propSig.remove();
|
|
655
936
|
return true;
|
|
@@ -661,10 +942,131 @@ function removeProperty(sourceFile, typeName, propertyName) {
|
|
|
661
942
|
}
|
|
662
943
|
return false;
|
|
663
944
|
}
|
|
945
|
+
function removeNeverReturnedType(sourceFile, functionName, neverReturnedType) {
|
|
946
|
+
const functions = sourceFile.getFunctions();
|
|
947
|
+
for (const func of functions) {
|
|
948
|
+
if (func.getName() === functionName) {
|
|
949
|
+
const returnTypeNode = func.getReturnTypeNode();
|
|
950
|
+
if (!returnTypeNode) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
const returnType = returnTypeNode.getType();
|
|
954
|
+
let typeToCheck = returnType;
|
|
955
|
+
let isPromise = false;
|
|
956
|
+
const symbol = returnType.getSymbol();
|
|
957
|
+
if (symbol?.getName() === "Promise") {
|
|
958
|
+
const typeArgs = returnType.getTypeArguments();
|
|
959
|
+
if (typeArgs.length > 0 && typeArgs[0]) {
|
|
960
|
+
typeToCheck = typeArgs[0];
|
|
961
|
+
isPromise = true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (!typeToCheck.isUnion()) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
const unionTypes = typeToCheck.getUnionTypes();
|
|
968
|
+
const hasInlineObjectType = unionTypes.some((ut) => ut.getSymbol()?.getName() === "__type");
|
|
969
|
+
const hasEnumLiteral = unionTypes.some((ut) => ut.isEnumLiteral());
|
|
970
|
+
if (hasInlineObjectType || hasEnumLiteral) {
|
|
971
|
+
const originalTypeText = returnTypeNode.getText();
|
|
972
|
+
let typeTextToModify = originalTypeText;
|
|
973
|
+
let promiseWrapper = "";
|
|
974
|
+
if (isPromise) {
|
|
975
|
+
const promiseMatch = originalTypeText.match(/^Promise<(.+)>$/s);
|
|
976
|
+
if (promiseMatch?.[1]) {
|
|
977
|
+
typeTextToModify = promiseMatch[1];
|
|
978
|
+
promiseWrapper = "Promise<>";
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const branches = splitUnionType(typeTextToModify);
|
|
982
|
+
const normalizedRemove = normalizeTypeText(neverReturnedType === "true" || neverReturnedType === "false" ? "boolean" : neverReturnedType);
|
|
983
|
+
const remainingBranches = branches.filter((branch) => {
|
|
984
|
+
const trimmed = branch.trim();
|
|
985
|
+
const normalized = normalizeTypeText(trimmed === "true" || trimmed === "false" ? "boolean" : trimmed);
|
|
986
|
+
return normalized !== normalizedRemove;
|
|
987
|
+
});
|
|
988
|
+
if (remainingBranches.length === 0 || remainingBranches.length === branches.length) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
let newReturnType = remainingBranches.join(" | ");
|
|
992
|
+
if (promiseWrapper) {
|
|
993
|
+
newReturnType = `Promise<${newReturnType}>`;
|
|
994
|
+
}
|
|
995
|
+
func.setReturnType(newReturnType);
|
|
996
|
+
return true;
|
|
997
|
+
} else {
|
|
998
|
+
const typesToKeep = [];
|
|
999
|
+
for (const ut of unionTypes) {
|
|
1000
|
+
const symbol2 = ut.getSymbol();
|
|
1001
|
+
const typeName = symbol2?.getName() || ut.getText();
|
|
1002
|
+
const normalizedName = typeName === "true" || typeName === "false" ? "boolean" : typeName;
|
|
1003
|
+
const normalizedRemove = neverReturnedType === "true" || neverReturnedType === "false" ? "boolean" : neverReturnedType;
|
|
1004
|
+
if (normalizedName !== normalizedRemove && !typesToKeep.includes(typeName)) {
|
|
1005
|
+
typesToKeep.push(typeName);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
let newReturnType;
|
|
1009
|
+
if (typesToKeep.length === 1 && typesToKeep[0]) {
|
|
1010
|
+
newReturnType = typesToKeep[0];
|
|
1011
|
+
} else if (typesToKeep.length > 1) {
|
|
1012
|
+
newReturnType = typesToKeep.join(" | ");
|
|
1013
|
+
}
|
|
1014
|
+
if (!newReturnType) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
if (isPromise) {
|
|
1018
|
+
newReturnType = `Promise<${newReturnType}>`;
|
|
1019
|
+
}
|
|
1020
|
+
func.setReturnType(newReturnType);
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
function splitUnionType(typeText) {
|
|
1028
|
+
const branches = [];
|
|
1029
|
+
let current = "";
|
|
1030
|
+
let depth = 0;
|
|
1031
|
+
let inString = false;
|
|
1032
|
+
let stringChar = "";
|
|
1033
|
+
for (let i = 0;i < typeText.length; i++) {
|
|
1034
|
+
const char = typeText[i];
|
|
1035
|
+
const prevChar = i > 0 ? typeText[i - 1] : "";
|
|
1036
|
+
if ((char === '"' || char === "'") && prevChar !== "\\") {
|
|
1037
|
+
if (!inString) {
|
|
1038
|
+
inString = true;
|
|
1039
|
+
stringChar = char;
|
|
1040
|
+
} else if (char === stringChar) {
|
|
1041
|
+
inString = false;
|
|
1042
|
+
stringChar = "";
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!inString) {
|
|
1046
|
+
if (char === "{" || char === "<" || char === "(") {
|
|
1047
|
+
depth++;
|
|
1048
|
+
} else if (char === "}" || char === ">" || char === ")") {
|
|
1049
|
+
depth--;
|
|
1050
|
+
} else if (char === "|" && depth === 0) {
|
|
1051
|
+
branches.push(current);
|
|
1052
|
+
current = "";
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
current += char;
|
|
1057
|
+
}
|
|
1058
|
+
if (current) {
|
|
1059
|
+
branches.push(current);
|
|
1060
|
+
}
|
|
1061
|
+
return branches;
|
|
1062
|
+
}
|
|
1063
|
+
function normalizeTypeText(typeText) {
|
|
1064
|
+
return typeText.replace(/;\s*([}\]>])/g, " $1").replace(/\s+/g, " ").trim();
|
|
1065
|
+
}
|
|
664
1066
|
function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results) {
|
|
665
1067
|
const sourceFiles = project.getSourceFiles();
|
|
666
1068
|
for (const sourceFile of sourceFiles) {
|
|
667
|
-
const relativePath =
|
|
1069
|
+
const relativePath = path10.relative(tsConfigDir, sourceFile.getFilePath());
|
|
668
1070
|
const absolutePath = sourceFile.getFilePath();
|
|
669
1071
|
if (filesWithChanges.has(absolutePath)) {
|
|
670
1072
|
continue;
|
|
@@ -676,7 +1078,7 @@ function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChang
|
|
|
676
1078
|
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
677
1079
|
if (moduleSpecifier) {
|
|
678
1080
|
const resolvedPath = resolveImportPath(sourceFile.getFilePath(), moduleSpecifier);
|
|
679
|
-
const relativeResolvedPath = resolvedPath ?
|
|
1081
|
+
const relativeResolvedPath = resolvedPath ? path10.relative(tsConfigDir, resolvedPath) : null;
|
|
680
1082
|
if (relativeResolvedPath && deletedFiles.has(relativeResolvedPath)) {
|
|
681
1083
|
brokenExports.push(exportDecl);
|
|
682
1084
|
} else {
|
|
@@ -724,9 +1126,9 @@ function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChang
|
|
|
724
1126
|
}
|
|
725
1127
|
}
|
|
726
1128
|
function resolveImportPath(fromFile, importPath) {
|
|
727
|
-
const fromDir =
|
|
1129
|
+
const fromDir = path10.dirname(fromFile);
|
|
728
1130
|
if (importPath.startsWith(".")) {
|
|
729
|
-
const resolved =
|
|
1131
|
+
const resolved = path10.resolve(fromDir, importPath);
|
|
730
1132
|
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
731
1133
|
for (const ext of extensions) {
|
|
732
1134
|
const withExt = resolved + ext;
|
|
@@ -735,7 +1137,7 @@ function resolveImportPath(fromFile, importPath) {
|
|
|
735
1137
|
}
|
|
736
1138
|
}
|
|
737
1139
|
for (const ext of extensions) {
|
|
738
|
-
const indexFile =
|
|
1140
|
+
const indexFile = path10.join(resolved, `index${ext}`);
|
|
739
1141
|
if (fs.existsSync(indexFile)) {
|
|
740
1142
|
return indexFile;
|
|
741
1143
|
}
|
|
@@ -746,7 +1148,7 @@ function resolveImportPath(fromFile, importPath) {
|
|
|
746
1148
|
}
|
|
747
1149
|
|
|
748
1150
|
// src/formatResults.ts
|
|
749
|
-
import
|
|
1151
|
+
import path11 from "node:path";
|
|
750
1152
|
function getSeverityMarker(severity) {
|
|
751
1153
|
const markers = {
|
|
752
1154
|
error: "[ERROR]",
|
|
@@ -768,12 +1170,16 @@ function formatPropertyLine(item) {
|
|
|
768
1170
|
const marker = getSeverityMarker(item.severity);
|
|
769
1171
|
return ` ${item.typeName}.${item.propertyName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (${status}${todoSuffix})`;
|
|
770
1172
|
}
|
|
1173
|
+
function formatNeverReturnedLine(item) {
|
|
1174
|
+
const marker = getSeverityMarker(item.severity);
|
|
1175
|
+
return ` ${item.functionName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (Type '${item.neverReturnedType}' in return type is never returned)`;
|
|
1176
|
+
}
|
|
771
1177
|
function formatGroupedItems(items, formatter, tsConfigDir, cwd) {
|
|
772
1178
|
const lines = [];
|
|
773
1179
|
const grouped = groupByFile(items);
|
|
774
1180
|
for (const [filePath, groupItems] of grouped.entries()) {
|
|
775
|
-
const absolutePath =
|
|
776
|
-
const relativePath =
|
|
1181
|
+
const absolutePath = path11.resolve(tsConfigDir, filePath);
|
|
1182
|
+
const relativePath = path11.relative(cwd, absolutePath);
|
|
777
1183
|
lines.push(relativePath);
|
|
778
1184
|
for (const item of groupItems) {
|
|
779
1185
|
lines.push(formatter(item));
|
|
@@ -796,8 +1202,8 @@ function formatResults(results, tsConfigDir) {
|
|
|
796
1202
|
lines.push("Completely Unused Files:");
|
|
797
1203
|
lines.push("");
|
|
798
1204
|
for (const filePath of results.unusedFiles) {
|
|
799
|
-
const absolutePath =
|
|
800
|
-
const relativePath =
|
|
1205
|
+
const absolutePath = path11.resolve(tsConfigDir, filePath);
|
|
1206
|
+
const relativePath = path11.relative(cwd, absolutePath);
|
|
801
1207
|
lines.push(relativePath);
|
|
802
1208
|
lines.push(" file:1:1-1 [ERROR] (All exports unused - file can be deleted)");
|
|
803
1209
|
lines.push("");
|
|
@@ -813,13 +1219,20 @@ function formatResults(results, tsConfigDir) {
|
|
|
813
1219
|
lines.push("");
|
|
814
1220
|
lines.push(...formatGroupedItems(propertiesToReport, formatPropertyLine, tsConfigDir, cwd));
|
|
815
1221
|
}
|
|
816
|
-
|
|
1222
|
+
const neverReturnedTypes = results.neverReturnedTypes || [];
|
|
1223
|
+
if (neverReturnedTypes.length > 0) {
|
|
1224
|
+
lines.push("Never-Returned Types:");
|
|
1225
|
+
lines.push("");
|
|
1226
|
+
lines.push(...formatGroupedItems(neverReturnedTypes, formatNeverReturnedLine, tsConfigDir, cwd));
|
|
1227
|
+
}
|
|
1228
|
+
if (results.unusedFiles.length === 0 && exportsToReport.length === 0 && propertiesToReport.length === 0 && neverReturnedTypes.length === 0) {
|
|
817
1229
|
lines.push("No unused exports or properties found!");
|
|
818
1230
|
} else {
|
|
819
1231
|
lines.push("Summary:");
|
|
820
1232
|
lines.push(` Completely unused files: ${results.unusedFiles.length}`);
|
|
821
1233
|
lines.push(` Unused exports: ${exportsToReport.length}`);
|
|
822
1234
|
lines.push(` Unused properties: ${propertiesToReport.length}`);
|
|
1235
|
+
lines.push(` Never-returned types: ${neverReturnedTypes.length}`);
|
|
823
1236
|
}
|
|
824
1237
|
return lines.join(`
|
|
825
1238
|
`);
|
|
@@ -860,11 +1273,11 @@ function main() {
|
|
|
860
1273
|
command = firstArg;
|
|
861
1274
|
configIndex = 1;
|
|
862
1275
|
}
|
|
863
|
-
let tsConfigPath =
|
|
1276
|
+
let tsConfigPath = path12.resolve(args[configIndex] ?? "");
|
|
864
1277
|
if (fs2.existsSync(tsConfigPath) && fs2.statSync(tsConfigPath).isDirectory()) {
|
|
865
|
-
tsConfigPath =
|
|
1278
|
+
tsConfigPath = path12.join(tsConfigPath, "tsconfig.json");
|
|
866
1279
|
}
|
|
867
|
-
const targetFilePath = args[configIndex + 1] ?
|
|
1280
|
+
const targetFilePath = args[configIndex + 1] ? path12.resolve(args[configIndex + 1]) : undefined;
|
|
868
1281
|
if (command === "fix") {
|
|
869
1282
|
console.log(`Fixing TypeScript project: ${tsConfigPath}`);
|
|
870
1283
|
console.log("");
|
|
@@ -903,11 +1316,11 @@ function main() {
|
|
|
903
1316
|
const filledLength = Math.min(barLength, Math.max(0, Math.floor(current / total * barLength)));
|
|
904
1317
|
const emptyLength = Math.max(0, barLength - filledLength);
|
|
905
1318
|
const bar = "█".repeat(filledLength) + "░".repeat(emptyLength);
|
|
906
|
-
const fileName =
|
|
1319
|
+
const fileName = path12.basename(filePath);
|
|
907
1320
|
process.stdout.write(`\r\x1B[KProgress: [${bar}] ${percentage}% (${current}/${total}) ${fileName}`);
|
|
908
1321
|
}, targetFilePath);
|
|
909
1322
|
process.stdout.write("\r\x1B[K");
|
|
910
|
-
const tsConfigDir =
|
|
1323
|
+
const tsConfigDir = path12.dirname(path12.resolve(tsConfigPath));
|
|
911
1324
|
const output = formatResults(results, tsConfigDir);
|
|
912
1325
|
console.log(output);
|
|
913
1326
|
}
|