ts-unused 1.0.2 → 1.0.3
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 +271 -1
- package/dist/analyzeFunctionReturnTypes.js +235 -0
- package/dist/analyzeInterfaces.d.ts +5 -1
- package/dist/analyzeInterfaces.js +51 -0
- package/dist/analyzeProject.d.ts +12 -1
- package/dist/analyzeProject.js +131 -0
- package/dist/analyzeTypeAliases.d.ts +7 -3
- package/dist/analyzeTypeAliases.js +70 -0
- package/dist/checkExportUsage.d.ts +5 -1
- package/dist/checkExportUsage.js +126 -0
- package/dist/checkGitStatus.js +49 -0
- package/dist/cli.js +267 -63
- package/dist/config.d.ts +96 -0
- package/dist/config.js +50 -0
- package/dist/extractTodoComment.js +27 -0
- package/dist/findNeverReturnedTypes.d.ts +4 -1
- package/dist/findNeverReturnedTypes.js +36 -0
- package/dist/findStructurallyEquivalentProperties.js +60 -0
- package/dist/findUnusedExports.d.ts +5 -1
- package/dist/findUnusedExports.js +36 -0
- package/dist/findUnusedProperties.d.ts +5 -1
- package/dist/findUnusedProperties.js +32 -0
- package/dist/fixProject.js +549 -0
- package/dist/formatResults.js +112 -0
- package/dist/hasNoCheck.js +8 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +9 -1251
- package/dist/isPropertyUnused.js +48 -0
- package/dist/isTestFile.d.ts +12 -0
- package/dist/isTestFile.js +23 -0
- package/dist/loadConfig.d.ts +20 -0
- package/dist/loadConfig.js +54 -0
- package/dist/patternMatcher.d.ts +26 -0
- package/dist/patternMatcher.js +83 -0
- package/dist/types.js +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -104,6 +104,103 @@ ts-unused ./tsconfig.json
|
|
|
104
104
|
|
|
105
105
|
# Auto-fix
|
|
106
106
|
ts-unused fix ./tsconfig.json
|
|
107
|
+
|
|
108
|
+
# With custom config path
|
|
109
|
+
ts-unused ./tsconfig.json --config ./custom.config.ts
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
Create an `unused.config.ts` file in your project root (same directory as `tsconfig.json`) to customize the analysis behavior.
|
|
115
|
+
|
|
116
|
+
### Example Configuration
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { defineConfig } from "ts-unused";
|
|
120
|
+
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
// Custom patterns for identifying test files
|
|
123
|
+
testFilePatterns: [
|
|
124
|
+
"**/*.test.ts",
|
|
125
|
+
"**/*.test.tsx",
|
|
126
|
+
"**/*.spec.ts",
|
|
127
|
+
"**/*.spec.tsx",
|
|
128
|
+
"**/__tests__/**",
|
|
129
|
+
"**/Test*.ts", // Include files like TestLogger.ts
|
|
130
|
+
],
|
|
131
|
+
|
|
132
|
+
// Files to completely ignore during analysis
|
|
133
|
+
ignoreFilePatterns: [
|
|
134
|
+
"**/generated/**",
|
|
135
|
+
"**/*.d.ts",
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
// Export names to ignore (supports glob patterns)
|
|
139
|
+
ignoreExports: [
|
|
140
|
+
"formatLogMessage", // Specific export
|
|
141
|
+
"internal*", // All exports starting with "internal"
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
// Property names to ignore in interfaces/types
|
|
145
|
+
ignoreProperties: [
|
|
146
|
+
"message", // Common error property
|
|
147
|
+
"_*", // Private-like properties
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
// Type names to skip property analysis for
|
|
151
|
+
ignoreTypes: [
|
|
152
|
+
"*Config", // Skip all config types
|
|
153
|
+
"Options",
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
// Whether to ignore module augmentation declarations
|
|
157
|
+
// (declare module "..." blocks)
|
|
158
|
+
ignoreModuleAugmentations: true,
|
|
159
|
+
|
|
160
|
+
// Toggle specific analysis features
|
|
161
|
+
analyzeExports: true,
|
|
162
|
+
analyzeProperties: true,
|
|
163
|
+
analyzeNeverReturnedTypes: true,
|
|
164
|
+
detectUnusedFiles: true,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Configuration Options
|
|
169
|
+
|
|
170
|
+
| Option | Type | Default | Description |
|
|
171
|
+
| --- | --- | --- | --- |
|
|
172
|
+
| `testFilePatterns` | `string[]` | `["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**"]` | Glob patterns for test file detection |
|
|
173
|
+
| `ignoreFilePatterns` | `string[]` | `[]` | Files to completely ignore during analysis |
|
|
174
|
+
| `ignoreExports` | `string[]` | `[]` | Export names to ignore (supports glob patterns) |
|
|
175
|
+
| `ignoreProperties` | `string[]` | `[]` | Property names to ignore (supports glob patterns) |
|
|
176
|
+
| `ignoreTypes` | `string[]` | `[]` | Type names to skip property analysis for |
|
|
177
|
+
| `ignoreModuleAugmentations` | `boolean` | `true` | Whether to ignore `declare module` blocks |
|
|
178
|
+
| `analyzeExports` | `boolean` | `true` | Enable/disable unused export detection |
|
|
179
|
+
| `analyzeProperties` | `boolean` | `true` | Enable/disable unused property detection |
|
|
180
|
+
| `analyzeNeverReturnedTypes` | `boolean` | `true` | Enable/disable never-returned type detection |
|
|
181
|
+
| `detectUnusedFiles` | `boolean` | `true` | Enable/disable completely unused file detection |
|
|
182
|
+
|
|
183
|
+
### Pattern Syntax
|
|
184
|
+
|
|
185
|
+
The configuration supports glob-like patterns:
|
|
186
|
+
|
|
187
|
+
- `*` - Matches any characters except `/`
|
|
188
|
+
- `**` - Matches any characters including `/`
|
|
189
|
+
- `?` - Matches any single character
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
- `**/*.test.ts` - All `.test.ts` files in any directory
|
|
193
|
+
- `**/Test*.ts` - All files starting with `Test` and ending with `.ts`
|
|
194
|
+
- `internal*` - Names starting with `internal`
|
|
195
|
+
- `*Config` - Names ending with `Config`
|
|
196
|
+
|
|
197
|
+
### CLI Options
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
ts-unused [command] <path-to-tsconfig.json> [options]
|
|
201
|
+
|
|
202
|
+
Options:
|
|
203
|
+
--config, -c <path> Path to configuration file (default: unused.config.ts in project dir)
|
|
107
204
|
```
|
|
108
205
|
|
|
109
206
|
## Output
|
|
@@ -355,6 +452,179 @@ function handler(sourceOpts: SourceOptions) {
|
|
|
355
452
|
|
|
356
453
|
In this case, `SourceOptions.timeout` and `SourceOptions.retryCount` are **not** flagged as unused, even though they're never directly accessed. The analyzer recognizes that `ProcessedOptions.timeout` and `ProcessedOptions.retryCount` are structurally equivalent and used, so their counterparts in `SourceOptions` are also considered used.
|
|
357
454
|
|
|
455
|
+
## Programmatic API
|
|
456
|
+
|
|
457
|
+
ts-unused can be used as a library in your own tools and scripts.
|
|
458
|
+
|
|
459
|
+
### Installation
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm install ts-unused
|
|
463
|
+
# or
|
|
464
|
+
bun add ts-unused
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Basic Usage
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import path from "node:path";
|
|
471
|
+
import { analyzeProject, loadConfigSync, formatResults } from "ts-unused";
|
|
472
|
+
|
|
473
|
+
// Given a project root with unused.config.ts
|
|
474
|
+
const projectRoot = "/path/to/your/project";
|
|
475
|
+
const tsConfigPath = path.join(projectRoot, "tsconfig.json");
|
|
476
|
+
|
|
477
|
+
// Load config from unused.config.ts (auto-detected in project directory)
|
|
478
|
+
const config = loadConfigSync(tsConfigPath);
|
|
479
|
+
|
|
480
|
+
// Analyze with config
|
|
481
|
+
const results = analyzeProject(tsConfigPath, undefined, undefined, { config });
|
|
482
|
+
|
|
483
|
+
// Format and print results (same output as CLI)
|
|
484
|
+
const output = formatResults(results, projectRoot);
|
|
485
|
+
console.log(output);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### API Reference
|
|
489
|
+
|
|
490
|
+
#### `analyzeProject(tsConfigPath, onProgress?, targetFilePath?, options?)`
|
|
491
|
+
|
|
492
|
+
Analyzes a TypeScript project for unused exports, properties, and never-returned types.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { analyzeProject, type AnalysisResults, type AnalyzeProjectOptions } from "ts-unused";
|
|
496
|
+
|
|
497
|
+
const options: AnalyzeProjectOptions = {
|
|
498
|
+
config: {
|
|
499
|
+
ignoreFilePatterns: ["**/generated/**"],
|
|
500
|
+
ignoreExports: ["internal*"],
|
|
501
|
+
analyzeProperties: true,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const results: AnalysisResults = analyzeProject(
|
|
506
|
+
"./tsconfig.json",
|
|
507
|
+
(current, total, filePath) => console.log(`Processing ${current}/${total}: ${filePath}`),
|
|
508
|
+
undefined, // targetFilePath - analyze all files
|
|
509
|
+
options
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
console.log(`Found ${results.unusedExports.length} unused exports`);
|
|
513
|
+
console.log(`Found ${results.unusedProperties.length} unused properties`);
|
|
514
|
+
console.log(`Found ${results.unusedFiles.length} completely unused files`);
|
|
515
|
+
console.log(`Found ${results.neverReturnedTypes?.length ?? 0} never-returned types`);
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Parameters:**
|
|
519
|
+
- `tsConfigPath` - Path to tsconfig.json
|
|
520
|
+
- `onProgress` - Optional callback for progress updates
|
|
521
|
+
- `targetFilePath` - Optional path to analyze a single file
|
|
522
|
+
- `options` - Optional `AnalyzeProjectOptions` object with `config` and/or `isTestFile`
|
|
523
|
+
|
|
524
|
+
**Returns:** `AnalysisResults` object with arrays of findings
|
|
525
|
+
|
|
526
|
+
#### `fixProject(tsConfigPath, onProgress?, isTestFile?)`
|
|
527
|
+
|
|
528
|
+
Automatically removes unused exports, properties, and deletes unused files.
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { fixProject, type FixResults } from "ts-unused";
|
|
532
|
+
|
|
533
|
+
const results: FixResults = fixProject(
|
|
534
|
+
"./tsconfig.json",
|
|
535
|
+
(message) => console.log(message)
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
console.log(`Fixed ${results.fixedExports} exports`);
|
|
539
|
+
console.log(`Fixed ${results.fixedProperties} properties`);
|
|
540
|
+
console.log(`Fixed ${results.fixedNeverReturnedTypes} never-returned types`);
|
|
541
|
+
console.log(`Deleted ${results.deletedFiles} files`);
|
|
542
|
+
console.log(`Skipped ${results.skippedFiles.length} files (git changes)`);
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
**Parameters:**
|
|
546
|
+
- `tsConfigPath` - Path to tsconfig.json
|
|
547
|
+
- `onProgress` - Optional callback for status messages
|
|
548
|
+
- `isTestFile` - Optional custom test file detection function
|
|
549
|
+
|
|
550
|
+
**Returns:** `FixResults` object with counts and lists of changes
|
|
551
|
+
|
|
552
|
+
#### `formatResults(results, tsConfigDir)`
|
|
553
|
+
|
|
554
|
+
Formats analysis results into a human-readable string (same format as CLI output).
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { analyzeProject, formatResults } from "ts-unused";
|
|
558
|
+
|
|
559
|
+
const results = analyzeProject("./tsconfig.json");
|
|
560
|
+
const formatted = formatResults(results, "./");
|
|
561
|
+
console.log(formatted);
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### `loadConfig(tsConfigPath)` / `loadConfigSync(tsConfigPath)`
|
|
565
|
+
|
|
566
|
+
Loads configuration from `unused.config.ts` in the project directory.
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
import { loadConfig, loadConfigSync, type UnusedConfig } from "ts-unused";
|
|
570
|
+
|
|
571
|
+
// Async version
|
|
572
|
+
const config: UnusedConfig = await loadConfig("./tsconfig.json");
|
|
573
|
+
|
|
574
|
+
// Sync version
|
|
575
|
+
const configSync: UnusedConfig = loadConfigSync("./tsconfig.json");
|
|
576
|
+
|
|
577
|
+
// Use with analyzeProject
|
|
578
|
+
const results = analyzeProject("./tsconfig.json", undefined, undefined, { config });
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
#### `defineConfig(config)`
|
|
582
|
+
|
|
583
|
+
Type-safe helper for creating configuration files.
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// unused.config.ts
|
|
587
|
+
import { defineConfig } from "ts-unused";
|
|
588
|
+
|
|
589
|
+
export default defineConfig({
|
|
590
|
+
ignoreFilePatterns: ["**/generated/**"],
|
|
591
|
+
ignoreExports: ["internal*"],
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### `createIsTestFile(patterns)`
|
|
596
|
+
|
|
597
|
+
Creates a custom test file detection function from glob patterns.
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
import { analyzeProject, createIsTestFile } from "ts-unused";
|
|
601
|
+
|
|
602
|
+
const isTestFile = createIsTestFile([
|
|
603
|
+
"**/*.test.ts",
|
|
604
|
+
"**/*.spec.ts",
|
|
605
|
+
"**/test/**",
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
const results = analyzeProject("./tsconfig.json", undefined, undefined, { isTestFile });
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
#### Pattern Matching Utilities
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import { matchesPattern, matchesFilePattern, patternToRegex } from "ts-unused";
|
|
615
|
+
|
|
616
|
+
// Match a name against patterns
|
|
617
|
+
matchesPattern("internalHelper", ["internal*"]); // true
|
|
618
|
+
matchesPattern("publicApi", ["internal*"]); // false
|
|
619
|
+
|
|
620
|
+
// Match a file path against patterns
|
|
621
|
+
matchesFilePattern("/project/src/generated/types.ts", ["**/generated/**"]); // true
|
|
622
|
+
|
|
623
|
+
// Convert a glob pattern to RegExp
|
|
624
|
+
const regex = patternToRegex("**/*.test.ts");
|
|
625
|
+
regex.test("src/utils.test.ts"); // true
|
|
626
|
+
```
|
|
627
|
+
|
|
358
628
|
## License
|
|
359
629
|
|
|
360
|
-
MIT License
|
|
630
|
+
MIT License
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
/**
|
|
4
|
+
* Analyzes a function to detect union type branches in the return type that are never actually returned
|
|
5
|
+
*/
|
|
6
|
+
export function analyzeFunctionReturnTypes(func, sourceFile, tsConfigDir) {
|
|
7
|
+
const results = [];
|
|
8
|
+
const functionName = func.getName();
|
|
9
|
+
// Skip functions without names
|
|
10
|
+
if (!functionName) {
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
// Get the return type node
|
|
14
|
+
const returnTypeNode = func.getReturnTypeNode();
|
|
15
|
+
if (!returnTypeNode) {
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
// Get the actual Type from the type checker
|
|
19
|
+
const returnType = returnTypeNode.getType();
|
|
20
|
+
// Try to unwrap Promise first
|
|
21
|
+
const unwrappedPromise = unwrapPromiseType(returnType);
|
|
22
|
+
const typeToCheck = unwrappedPromise || returnType;
|
|
23
|
+
// Check if it's a union type (or contains a union after unwrapping Promise)
|
|
24
|
+
if (!typeToCheck.isUnion()) {
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
// Get all union type branches
|
|
28
|
+
const unionTypes = typeToCheck.getUnionTypes();
|
|
29
|
+
if (unionTypes.length < 2) {
|
|
30
|
+
// Not a meaningful union
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
// Get all return statements in the function
|
|
34
|
+
const returnStatements = func.getDescendantsOfKind(SyntaxKind.ReturnStatement);
|
|
35
|
+
// Collect types of all returned values
|
|
36
|
+
const returnedTypes = [];
|
|
37
|
+
for (const returnStmt of returnStatements) {
|
|
38
|
+
const expression = returnStmt.getExpression();
|
|
39
|
+
if (expression) {
|
|
40
|
+
const exprType = expression.getType();
|
|
41
|
+
// Unwrap Promise if needed
|
|
42
|
+
const unwrapped = unwrapPromiseType(exprType);
|
|
43
|
+
returnedTypes.push(unwrapped || exprType);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// If no return statements, can't determine what's returned
|
|
47
|
+
if (returnedTypes.length === 0) {
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
// Check if any returned type is a generic type assignable to the whole union but not to specific branches
|
|
51
|
+
// This happens with destructured objects or other inferred types that satisfy the union structurally
|
|
52
|
+
for (const returnedType of returnedTypes) {
|
|
53
|
+
if (returnedType.isAssignableTo(typeToCheck)) {
|
|
54
|
+
// Check if this type is assignable to ANY specific union branch
|
|
55
|
+
let assignableToAnyBranch = false;
|
|
56
|
+
for (const unionBranch of unionTypes) {
|
|
57
|
+
if (returnedType.isAssignableTo(unionBranch)) {
|
|
58
|
+
assignableToAnyBranch = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// If assignable to the union but NOT to any specific branch,
|
|
63
|
+
// this is a generic type that could be any branch - skip analysis
|
|
64
|
+
if (!assignableToAnyBranch) {
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Group enum literals by their parent enum
|
|
70
|
+
// If any enum member is returned, we consider the entire enum as "returned"
|
|
71
|
+
const enumGroups = new Map();
|
|
72
|
+
const enumsWithReturnedValues = new Set();
|
|
73
|
+
for (const unionBranch of unionTypes) {
|
|
74
|
+
if (unionBranch.isEnumLiteral()) {
|
|
75
|
+
const enumName = getEnumNameFromLiteral(unionBranch);
|
|
76
|
+
if (enumName) {
|
|
77
|
+
if (!enumGroups.has(enumName)) {
|
|
78
|
+
enumGroups.set(enumName, []);
|
|
79
|
+
}
|
|
80
|
+
enumGroups.get(enumName)?.push(unionBranch);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Check which enums have at least one returned value
|
|
85
|
+
for (const [enumName, enumBranches] of enumGroups.entries()) {
|
|
86
|
+
for (const returnedType of returnedTypes) {
|
|
87
|
+
// Check if this returned type is any member of this enum
|
|
88
|
+
for (const enumBranch of enumBranches) {
|
|
89
|
+
if (isTypeAssignableTo(returnedType, enumBranch)) {
|
|
90
|
+
enumsWithReturnedValues.add(enumName);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (enumsWithReturnedValues.has(enumName)) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Group union branches by display name to handle boolean (true/false) and avoid duplicates
|
|
100
|
+
const branchMap = new Map();
|
|
101
|
+
for (const unionBranch of unionTypes) {
|
|
102
|
+
const displayName = getTypeDisplayName(unionBranch);
|
|
103
|
+
if (!branchMap.has(displayName)) {
|
|
104
|
+
branchMap.set(displayName, unionBranch);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check each unique union branch to see if it's ever returned
|
|
108
|
+
for (const [displayName, unionBranch] of branchMap.entries()) {
|
|
109
|
+
// Skip enum literal branches if the enum has at least one returned value
|
|
110
|
+
if (unionBranch.isEnumLiteral()) {
|
|
111
|
+
const enumName = getEnumNameFromLiteral(unionBranch);
|
|
112
|
+
if (enumName && enumsWithReturnedValues.has(enumName)) {
|
|
113
|
+
// This enum has at least one returned value, so don't flag any of its members
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let isReturned = false;
|
|
118
|
+
for (const returnedType of returnedTypes) {
|
|
119
|
+
// Check if the returned type is assignable to this union branch
|
|
120
|
+
if (isTypeAssignableTo(returnedType, unionBranch)) {
|
|
121
|
+
isReturned = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!isReturned) {
|
|
126
|
+
// This union branch is never returned
|
|
127
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
128
|
+
const nameNode = func.getNameNode();
|
|
129
|
+
if (nameNode) {
|
|
130
|
+
const startPos = nameNode.getStart();
|
|
131
|
+
const lineStartPos = nameNode.getStartLinePos();
|
|
132
|
+
const character = startPos - lineStartPos + 1;
|
|
133
|
+
const endCharacter = character + functionName.length;
|
|
134
|
+
results.push({
|
|
135
|
+
filePath: relativePath,
|
|
136
|
+
functionName,
|
|
137
|
+
neverReturnedType: displayName,
|
|
138
|
+
line: nameNode.getStartLineNumber(),
|
|
139
|
+
character,
|
|
140
|
+
endCharacter,
|
|
141
|
+
severity: "error",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Unwrap Promise<T> to get T
|
|
150
|
+
*/
|
|
151
|
+
function unwrapPromiseType(type) {
|
|
152
|
+
const symbol = type.getSymbol();
|
|
153
|
+
if (symbol?.getName() === "Promise") {
|
|
154
|
+
const typeArgs = type.getTypeArguments();
|
|
155
|
+
if (typeArgs.length > 0 && typeArgs[0]) {
|
|
156
|
+
return typeArgs[0];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if sourceType is assignable to targetType
|
|
163
|
+
*/
|
|
164
|
+
function isTypeAssignableTo(sourceType, targetType) {
|
|
165
|
+
// Use TypeScript's built-in type checker for assignability
|
|
166
|
+
// This properly handles structural compatibility, inferred types, etc.
|
|
167
|
+
if (sourceType.isAssignableTo(targetType)) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Special handling for boolean literals (true/false)
|
|
171
|
+
// TypeScript represents boolean as true | false, so we need to check
|
|
172
|
+
// if both are boolean literals when comparing
|
|
173
|
+
const sourceText = sourceType.getText();
|
|
174
|
+
const targetText = targetType.getText();
|
|
175
|
+
const isBooleanLiteral = (text) => text === "true" || text === "false";
|
|
176
|
+
if (isBooleanLiteral(sourceText) && isBooleanLiteral(targetText)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Extract the enum name from an enum literal type
|
|
183
|
+
* For example: "Status.Idle" -> "Status"
|
|
184
|
+
*/
|
|
185
|
+
function getEnumNameFromLiteral(type) {
|
|
186
|
+
if (!type.isEnumLiteral()) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const typeText = type.getText();
|
|
190
|
+
// Strip import paths first
|
|
191
|
+
const cleanedText = typeText.replace(/import\([^)]+\)\./g, "");
|
|
192
|
+
// Extract enum name from "EnumName.MemberName"
|
|
193
|
+
const lastDotIndex = cleanedText.lastIndexOf(".");
|
|
194
|
+
if (lastDotIndex > 0) {
|
|
195
|
+
return cleanedText.substring(0, lastDotIndex);
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get a readable display name for a type
|
|
201
|
+
*/
|
|
202
|
+
function getTypeDisplayName(type) {
|
|
203
|
+
const symbol = type.getSymbol();
|
|
204
|
+
// If it has a symbol with a name, use that
|
|
205
|
+
if (symbol) {
|
|
206
|
+
const name = symbol.getName();
|
|
207
|
+
if (name && name !== "__type") {
|
|
208
|
+
return name;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Otherwise, use the type text
|
|
212
|
+
let typeText = type.getText();
|
|
213
|
+
// Strip import paths from type text
|
|
214
|
+
// Pattern: import("path/to/file").TypeName -> TypeName
|
|
215
|
+
typeText = typeText.replace(/import\([^)]+\)\./g, "");
|
|
216
|
+
// Simplify common type texts
|
|
217
|
+
if (typeText === "string")
|
|
218
|
+
return "string";
|
|
219
|
+
if (typeText === "number")
|
|
220
|
+
return "number";
|
|
221
|
+
if (typeText === "boolean")
|
|
222
|
+
return "boolean";
|
|
223
|
+
if (typeText === "true" || typeText === "false")
|
|
224
|
+
return "boolean";
|
|
225
|
+
if (typeText === "null")
|
|
226
|
+
return "null";
|
|
227
|
+
if (typeText === "undefined")
|
|
228
|
+
return "undefined";
|
|
229
|
+
// Truncate very long types (e.g., inline object types)
|
|
230
|
+
const MAX_TYPE_LENGTH = 100;
|
|
231
|
+
if (typeText.length > MAX_TYPE_LENGTH) {
|
|
232
|
+
return `${typeText.substring(0, MAX_TYPE_LENGTH)}...`;
|
|
233
|
+
}
|
|
234
|
+
return typeText;
|
|
235
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { Project, SourceFile } from "ts-morph";
|
|
2
2
|
import type { IsTestFileFn, UnusedPropertyResult } from "./types";
|
|
3
|
-
export
|
|
3
|
+
export interface AnalyzeInterfacesOptions {
|
|
4
|
+
ignoreProperties?: string[];
|
|
5
|
+
ignoreTypes?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function analyzeInterfaces(sourceFile: SourceFile, tsConfigDir: string, isTestFile: IsTestFileFn, results: UnusedPropertyResult[], project: Project, options?: AnalyzeInterfacesOptions): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { extractTodoComment } from "./extractTodoComment";
|
|
3
|
+
import { isPropertyUnused } from "./isPropertyUnused";
|
|
4
|
+
import { matchesPattern } from "./patternMatcher";
|
|
5
|
+
export function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
6
|
+
const { ignoreProperties = [], ignoreTypes = [] } = options;
|
|
7
|
+
const interfaces = sourceFile.getInterfaces();
|
|
8
|
+
for (const iface of interfaces) {
|
|
9
|
+
const interfaceName = iface.getName();
|
|
10
|
+
// Skip ignored types
|
|
11
|
+
if (ignoreTypes.length > 0 && matchesPattern(interfaceName, ignoreTypes)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
for (const prop of iface.getProperties()) {
|
|
15
|
+
const propertyName = prop.getName();
|
|
16
|
+
// Skip ignored properties
|
|
17
|
+
if (ignoreProperties.length > 0 && matchesPattern(propertyName, ignoreProperties)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const usage = isPropertyUnused(prop, isTestFile, project);
|
|
21
|
+
if (usage.isUnusedOrTestOnly) {
|
|
22
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
23
|
+
const todoComment = extractTodoComment(prop);
|
|
24
|
+
// Determine severity: warning for TODO, info for test-only, error for completely unused
|
|
25
|
+
let severity = "error";
|
|
26
|
+
if (todoComment) {
|
|
27
|
+
severity = "warning";
|
|
28
|
+
}
|
|
29
|
+
else if (usage.onlyUsedInTests) {
|
|
30
|
+
severity = "info";
|
|
31
|
+
}
|
|
32
|
+
const startPos = prop.getStart();
|
|
33
|
+
const lineStartPos = prop.getStartLinePos();
|
|
34
|
+
const character = startPos - lineStartPos + 1;
|
|
35
|
+
const endCharacter = character + propertyName.length;
|
|
36
|
+
const result = {
|
|
37
|
+
filePath: relativePath,
|
|
38
|
+
typeName: interfaceName,
|
|
39
|
+
propertyName,
|
|
40
|
+
line: prop.getStartLineNumber(),
|
|
41
|
+
character,
|
|
42
|
+
endCharacter,
|
|
43
|
+
todoComment,
|
|
44
|
+
severity,
|
|
45
|
+
onlyUsedInTests: usage.onlyUsedInTests,
|
|
46
|
+
};
|
|
47
|
+
results.push(result);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/analyzeProject.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
+
import { type UnusedConfig } from "./config";
|
|
1
2
|
import type { AnalysisResults, IsTestFileFn } from "./types";
|
|
2
|
-
export
|
|
3
|
+
export interface AnalyzeProjectOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the analysis.
|
|
6
|
+
*/
|
|
7
|
+
config?: UnusedConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Custom test file detection function (overrides config.testFilePatterns).
|
|
10
|
+
*/
|
|
11
|
+
isTestFile?: IsTestFileFn;
|
|
12
|
+
}
|
|
13
|
+
export declare function analyzeProject(tsConfigPath: string, onProgress?: (current: number, total: number, filePath: string) => void, targetFilePath?: string, isTestFileOrOptions?: IsTestFileFn | AnalyzeProjectOptions): AnalysisResults;
|