ts-unused 1.0.0 → 1.0.1
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/cli.js +477 -64
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ A CLI tool that analyzes TypeScript projects to find unused exports and unused p
|
|
|
9
9
|
- **Finds Unused Exports**: Identifies functions, classes, types, interfaces, and constants that are exported but never imported or used elsewhere
|
|
10
10
|
- **Finds Completely Unused Files**: Identifies files where all exports are unused, suggesting the entire file can be deleted
|
|
11
11
|
- **Finds Unused Type Properties**: Detects properties in interfaces and type aliases that are defined but never accessed
|
|
12
|
-
- **
|
|
12
|
+
- **Finds Never-Returned Types**: Detects union type branches in function return types that are declared but never actually returned
|
|
13
|
+
- **Auto-Fix Command**: Automatically removes unused exports, properties, deletes unused files, and fixes never-returned types with git safety checks
|
|
13
14
|
- **Structural Property Equivalence**: Handles property re-declarations across multiple interfaces - properties are considered "used" if structurally equivalent properties (same name and type) are accessed in any interface
|
|
14
15
|
- **Three-Tier Severity System**: Categorizes findings by severity level for better prioritization
|
|
15
16
|
- **ERROR**: Completely unused code that should be removed
|
|
@@ -36,6 +37,7 @@ A CLI tool that analyzes TypeScript projects to find unused exports and unused p
|
|
|
36
37
|
| **Goal** | **Report & Analyze** | **Remove (Tree-Shake)** | **Report** |
|
|
37
38
|
| **Unused Exports** | ✅ Detects and reports | ✅ Detects and removes | ✅ Detects and reports |
|
|
38
39
|
| **Unused Properties** | ✅ **Unique:** Checks `interface`/`type` properties | ❌ No property checks | ❌ No property checks |
|
|
40
|
+
| **Never-Returned Types** | ✅ **Unique:** Detects unused union branches | ❌ Not supported | ❌ Not supported |
|
|
39
41
|
| **Test-Only Usage** | ✅ **Unique:** Identifies "Used only in tests" | ⚠️ May delete if not entrypoint | ❌ No distinction |
|
|
40
42
|
| **Comment Support** | ✅ **Unique:** TODOs change severity | ✅ Skip/Ignore only | ✅ Skip/Ignore only |
|
|
41
43
|
| **Unused Files** | ✅ Reports completely unused files | ✅ Deletes unreachable files | ✅ Explicit report flag |
|
|
@@ -113,7 +115,8 @@ The script outputs:
|
|
|
113
115
|
1. **Completely Unused Files**: Files where all exports are unused (candidates for deletion)
|
|
114
116
|
2. **Unused Exports**: Functions, types, interfaces, and constants that are exported but never used
|
|
115
117
|
3. **Unused Properties**: Properties in types/interfaces that are defined but never accessed
|
|
116
|
-
|
|
118
|
+
4. **Never-Returned Types**: Union type branches in function return types that are never actually returned
|
|
119
|
+
5. **Summary**: Total count of unused items found
|
|
117
120
|
|
|
118
121
|
Each finding includes:
|
|
119
122
|
- File path relative to the project root
|
|
@@ -144,9 +147,16 @@ packages/example/src/types.ts
|
|
|
144
147
|
UserConfig.futureFeature:12:3-16 [WARNING] (Unused property: [TODO] implement this later)
|
|
145
148
|
TestHelpers.mockData:8:3-11 [INFO] (Used only in tests)
|
|
146
149
|
|
|
150
|
+
Never-Returned Types:
|
|
151
|
+
|
|
152
|
+
packages/example/src/api.ts
|
|
153
|
+
processRequest.ErrorResult:15:17-28 [ERROR] (Never-returned type in union)
|
|
154
|
+
fetchData.TimeoutError:42:25-37 [ERROR] (Never-returned type in union)
|
|
155
|
+
|
|
147
156
|
Summary:
|
|
148
157
|
Unused exports: 3
|
|
149
158
|
Unused properties: 3
|
|
159
|
+
Never-returned types: 2
|
|
150
160
|
```
|
|
151
161
|
|
|
152
162
|
### Example Fix Output
|
|
@@ -159,11 +169,15 @@ Fixing: src/helpers.ts
|
|
|
159
169
|
Removed unused export: unusedFunction
|
|
160
170
|
Fixing: src/types.ts
|
|
161
171
|
Removed unused property: UserConfig.unusedProp
|
|
172
|
+
Fixing: src/api.ts
|
|
173
|
+
✓ Removed never-returned type 'ErrorResult' from processRequest
|
|
174
|
+
✓ Removed never-returned type 'TimeoutError' from fetchData
|
|
162
175
|
Skipped: src/modified.ts (has local git changes)
|
|
163
176
|
|
|
164
177
|
Summary:
|
|
165
178
|
Fixed exports: 1
|
|
166
179
|
Fixed properties: 1
|
|
180
|
+
Fixed never-returned types: 2
|
|
167
181
|
Deleted files: 1
|
|
168
182
|
Skipped files: 1
|
|
169
183
|
|
|
@@ -253,6 +267,57 @@ This will:
|
|
|
253
267
|
|
|
254
268
|
## Advanced Features
|
|
255
269
|
|
|
270
|
+
### Never-Returned Types Detection
|
|
271
|
+
|
|
272
|
+
The analyzer detects when functions declare union types in their return type signature but never actually return certain branches of that union. This commonly occurs when:
|
|
273
|
+
|
|
274
|
+
- **Error Handling**: Functions declare error types but never return errors in practice
|
|
275
|
+
- **Refactoring**: Code evolution leaves unused type branches behind
|
|
276
|
+
- **Over-Engineering**: Return types are declared too broadly for actual usage
|
|
277
|
+
|
|
278
|
+
**Example:**
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
interface SuccessResult {
|
|
282
|
+
success: true;
|
|
283
|
+
data: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface ErrorResult {
|
|
287
|
+
success: false;
|
|
288
|
+
error: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type ResultType = SuccessResult | ErrorResult;
|
|
292
|
+
|
|
293
|
+
// ErrorResult is never returned
|
|
294
|
+
export function processData(): ResultType {
|
|
295
|
+
// This function only ever returns SuccessResult
|
|
296
|
+
return { success: true, data: "processed" };
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Detection Capabilities:**
|
|
301
|
+
- Analyzes all function return statements to determine actual return types
|
|
302
|
+
- Handles `Promise<Union>` types for async functions
|
|
303
|
+
- Works with type aliases and direct union declarations
|
|
304
|
+
- Supports primitive unions (`string | number | boolean`)
|
|
305
|
+
- Handles multi-way unions (3+ types)
|
|
306
|
+
- Normalizes boolean literals (`true | false` → `boolean`)
|
|
307
|
+
|
|
308
|
+
**Auto-Fix Behavior:**
|
|
309
|
+
- Removes never-returned types from the union declaration
|
|
310
|
+
- Preserves `Promise<>` wrapper for async functions
|
|
311
|
+
- Simplifies to single type when only one branch remains
|
|
312
|
+
- Skips functions that are completely unused (they get removed entirely)
|
|
313
|
+
|
|
314
|
+
After fixing the example above, the return type would become:
|
|
315
|
+
```typescript
|
|
316
|
+
export function processData(): SuccessResult {
|
|
317
|
+
return { success: true, data: "processed" };
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
256
321
|
### Structural Property Equivalence
|
|
257
322
|
|
|
258
323
|
The analyzer handles cases where properties are re-declared across multiple interfaces with the same name and type. This commonly occurs with:
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-unused",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Find unused exports and properties in TypeScript projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"clean": "rm -rf dist",
|
|
15
15
|
"build:js": "bun build src/cli.ts --outdir dist --target node --external ts-morph --external typescript",
|
|
16
|
-
"build": "bun
|
|
16
|
+
"build": "bun clean && bun build:js",
|
|
17
17
|
"typecheck": "tsgo --noEmit",
|
|
18
18
|
"lint": "bun run fix && biome check src",
|
|
19
19
|
"fix": "biome check --write --unsafe src",
|
|
20
|
-
"sanity-check": "bun run scripts/sanity-check.ts"
|
|
20
|
+
"sanity-check": "bun run scripts/sanity-check.ts",
|
|
21
|
+
"prepublish": "bun lint && bun test && bun run build && bun sanity-check --no-build",
|
|
22
|
+
"cli": "bun run src/cli.ts"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@biomejs/biome": "^2.3.8",
|