ts-unused 1.0.1 → 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.d.ts +6 -0
- package/dist/analyzeFunctionReturnTypes.js +235 -0
- package/dist/analyzeInterfaces.d.ts +7 -0
- package/dist/analyzeInterfaces.js +51 -0
- package/dist/analyzeProject.d.ts +13 -0
- package/dist/analyzeProject.js +131 -0
- package/dist/analyzeTypeAliases.d.ts +9 -0
- package/dist/analyzeTypeAliases.js +70 -0
- package/dist/checkExportUsage.d.ts +7 -0
- package/dist/checkExportUsage.js +126 -0
- package/dist/checkGitStatus.d.ts +7 -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.d.ts +6 -0
- package/dist/extractTodoComment.js +27 -0
- package/dist/findNeverReturnedTypes.d.ts +6 -0
- package/dist/findNeverReturnedTypes.js +36 -0
- package/dist/findStructurallyEquivalentProperties.d.ts +12 -0
- package/dist/findStructurallyEquivalentProperties.js +60 -0
- package/dist/findUnusedExports.d.ts +7 -0
- package/dist/findUnusedExports.js +36 -0
- package/dist/findUnusedProperties.d.ts +7 -0
- package/dist/findUnusedProperties.js +32 -0
- package/dist/fixProject.d.ts +13 -0
- package/dist/fixProject.js +549 -0
- package/dist/formatResults.d.ts +2 -0
- package/dist/formatResults.js +112 -0
- package/dist/hasNoCheck.d.ts +2 -0
- package/dist/hasNoCheck.js +8 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/isPropertyUnused.d.ts +7 -0
- package/dist/isPropertyUnused.js +48 -0
- package/dist/isTestFile.d.ts +14 -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.d.ts +40 -0
- package/dist/types.js +1 -0
- package/package.json +12 -3
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
+
import fs3 from "node:fs";
|
|
7
|
+
import path13 from "node:path";
|
|
6
8
|
|
|
7
9
|
// src/analyzeProject.ts
|
|
8
10
|
import path8 from "node:path";
|
|
9
11
|
import { Project } from "ts-morph";
|
|
10
12
|
|
|
13
|
+
// src/config.ts
|
|
14
|
+
var defaultConfig = {
|
|
15
|
+
testFilePatterns: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**"],
|
|
16
|
+
ignoreFilePatterns: [],
|
|
17
|
+
ignoreExports: [],
|
|
18
|
+
ignoreProperties: [],
|
|
19
|
+
ignoreTypes: [],
|
|
20
|
+
ignoreModuleAugmentations: true,
|
|
21
|
+
analyzeProperties: true,
|
|
22
|
+
analyzeExports: true,
|
|
23
|
+
analyzeNeverReturnedTypes: true,
|
|
24
|
+
detectUnusedFiles: true
|
|
25
|
+
};
|
|
26
|
+
function mergeConfig(userConfig) {
|
|
27
|
+
const result = {
|
|
28
|
+
testFilePatterns: userConfig.testFilePatterns ?? defaultConfig.testFilePatterns,
|
|
29
|
+
ignoreFilePatterns: userConfig.ignoreFilePatterns ?? defaultConfig.ignoreFilePatterns,
|
|
30
|
+
ignoreExports: userConfig.ignoreExports ?? defaultConfig.ignoreExports,
|
|
31
|
+
ignoreProperties: userConfig.ignoreProperties ?? defaultConfig.ignoreProperties,
|
|
32
|
+
ignoreTypes: userConfig.ignoreTypes ?? defaultConfig.ignoreTypes,
|
|
33
|
+
ignoreModuleAugmentations: userConfig.ignoreModuleAugmentations ?? defaultConfig.ignoreModuleAugmentations,
|
|
34
|
+
analyzeProperties: userConfig.analyzeProperties ?? defaultConfig.analyzeProperties,
|
|
35
|
+
analyzeExports: userConfig.analyzeExports ?? defaultConfig.analyzeExports,
|
|
36
|
+
analyzeNeverReturnedTypes: userConfig.analyzeNeverReturnedTypes ?? defaultConfig.analyzeNeverReturnedTypes,
|
|
37
|
+
detectUnusedFiles: userConfig.detectUnusedFiles ?? defaultConfig.detectUnusedFiles
|
|
38
|
+
};
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
// src/findNeverReturnedTypes.ts
|
|
12
43
|
import path2 from "node:path";
|
|
13
44
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
@@ -205,8 +236,58 @@ function hasNoCheck(sourceFile) {
|
|
|
205
236
|
return firstLine.trim() === "// @ts-nocheck";
|
|
206
237
|
}
|
|
207
238
|
|
|
239
|
+
// src/patternMatcher.ts
|
|
240
|
+
function patternToRegex(pattern) {
|
|
241
|
+
let regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
const DOUBLE_STAR_PLACEHOLDER = "<<DOUBLESTAR>>";
|
|
243
|
+
regexStr = regexStr.replace(/\*\*/g, DOUBLE_STAR_PLACEHOLDER);
|
|
244
|
+
regexStr = regexStr.replace(/\*/g, "[^/]*");
|
|
245
|
+
regexStr = regexStr.split(DOUBLE_STAR_PLACEHOLDER).join(".*");
|
|
246
|
+
regexStr = regexStr.replace(/\?/g, ".");
|
|
247
|
+
return new RegExp(`^${regexStr}$`);
|
|
248
|
+
}
|
|
249
|
+
function matchesPattern(value, patterns) {
|
|
250
|
+
for (const pattern of patterns) {
|
|
251
|
+
if (pattern === value) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (pattern.includes("*") || pattern.includes("?")) {
|
|
255
|
+
const regex = patternToRegex(pattern);
|
|
256
|
+
if (regex.test(value)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
function matchesFilePattern(filePath, patterns) {
|
|
264
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
265
|
+
for (const pattern of patterns) {
|
|
266
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
267
|
+
if (normalizedPattern.startsWith("**/")) {
|
|
268
|
+
const suffixPattern = normalizedPattern.slice(3);
|
|
269
|
+
const regex = patternToRegex(`**/${suffixPattern}`);
|
|
270
|
+
if (regex.test(normalizedPath)) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const suffixRegex = patternToRegex(suffixPattern);
|
|
274
|
+
const fileName = normalizedPath.split("/").pop() ?? "";
|
|
275
|
+
if (suffixRegex.test(fileName)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
const regex = patternToRegex(normalizedPattern);
|
|
280
|
+
if (regex.test(normalizedPath)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
208
288
|
// src/findNeverReturnedTypes.ts
|
|
209
|
-
function findNeverReturnedTypes(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
289
|
+
function findNeverReturnedTypes(project, tsConfigDir, isTestFile, onProgress, targetFilePath, options = {}) {
|
|
290
|
+
const { ignoreFilePatterns = [] } = options;
|
|
210
291
|
const results = [];
|
|
211
292
|
for (const sourceFile of project.getSourceFiles()) {
|
|
212
293
|
if (isTestFile(sourceFile)) {
|
|
@@ -215,11 +296,15 @@ function findNeverReturnedTypes(project, tsConfigDir, isTestFile, onProgress, ta
|
|
|
215
296
|
if (hasNoCheck(sourceFile)) {
|
|
216
297
|
continue;
|
|
217
298
|
}
|
|
218
|
-
|
|
299
|
+
const filePath = sourceFile.getFilePath();
|
|
300
|
+
if (targetFilePath && filePath !== targetFilePath) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (ignoreFilePatterns.length > 0 && matchesFilePattern(filePath, ignoreFilePatterns)) {
|
|
219
304
|
continue;
|
|
220
305
|
}
|
|
221
306
|
if (onProgress) {
|
|
222
|
-
const relativePath = path2.relative(tsConfigDir,
|
|
307
|
+
const relativePath = path2.relative(tsConfigDir, filePath);
|
|
223
308
|
onProgress(relativePath);
|
|
224
309
|
}
|
|
225
310
|
const functions = sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionDeclaration);
|
|
@@ -274,11 +359,27 @@ function getNameNode(declaration) {
|
|
|
274
359
|
}
|
|
275
360
|
return;
|
|
276
361
|
}
|
|
277
|
-
function
|
|
362
|
+
function isModuleAugmentation(declaration) {
|
|
363
|
+
if (Node.isModuleDeclaration(declaration)) {
|
|
364
|
+
const nameNode = declaration.getNameNode();
|
|
365
|
+
if (Node.isStringLiteral(nameNode)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile, options = {}) {
|
|
372
|
+
const { ignoreExports = [], ignoreModuleAugmentations = true } = options;
|
|
278
373
|
const firstDeclaration = declarations[0];
|
|
279
374
|
if (!firstDeclaration) {
|
|
280
375
|
return null;
|
|
281
376
|
}
|
|
377
|
+
if (ignoreExports.length > 0 && matchesPattern(exportName, ignoreExports)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (ignoreModuleAugmentations && isModuleAugmentation(firstDeclaration)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
282
383
|
const declarationSourceFile = firstDeclaration.getSourceFile();
|
|
283
384
|
if (declarationSourceFile.getFilePath() !== sourceFile.getFilePath()) {
|
|
284
385
|
return null;
|
|
@@ -329,7 +430,8 @@ function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isT
|
|
|
329
430
|
}
|
|
330
431
|
|
|
331
432
|
// src/findUnusedExports.ts
|
|
332
|
-
function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
433
|
+
function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetFilePath, options = {}) {
|
|
434
|
+
const { ignoreFilePatterns = [], ...checkOptions } = options;
|
|
333
435
|
const results = [];
|
|
334
436
|
for (const sourceFile of project.getSourceFiles()) {
|
|
335
437
|
if (isTestFile(sourceFile)) {
|
|
@@ -338,16 +440,20 @@ function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetF
|
|
|
338
440
|
if (hasNoCheck(sourceFile)) {
|
|
339
441
|
continue;
|
|
340
442
|
}
|
|
341
|
-
|
|
443
|
+
const filePath = sourceFile.getFilePath();
|
|
444
|
+
if (targetFilePath && filePath !== targetFilePath) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (ignoreFilePatterns.length > 0 && matchesFilePattern(filePath, ignoreFilePatterns)) {
|
|
342
448
|
continue;
|
|
343
449
|
}
|
|
344
450
|
if (onProgress) {
|
|
345
|
-
const relativePath = path4.relative(tsConfigDir,
|
|
451
|
+
const relativePath = path4.relative(tsConfigDir, filePath);
|
|
346
452
|
onProgress(relativePath);
|
|
347
453
|
}
|
|
348
454
|
const exports = sourceFile.getExportedDeclarations();
|
|
349
455
|
for (const [exportName, declarations] of exports.entries()) {
|
|
350
|
-
const unusedExport = checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile);
|
|
456
|
+
const unusedExport = checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile, checkOptions);
|
|
351
457
|
if (unusedExport) {
|
|
352
458
|
results.push(unusedExport);
|
|
353
459
|
}
|
|
@@ -483,11 +589,19 @@ function isPropertyUnused(prop, isTestFile, project) {
|
|
|
483
589
|
}
|
|
484
590
|
|
|
485
591
|
// src/analyzeInterfaces.ts
|
|
486
|
-
function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
592
|
+
function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
593
|
+
const { ignoreProperties = [], ignoreTypes = [] } = options;
|
|
487
594
|
const interfaces = sourceFile.getInterfaces();
|
|
488
595
|
for (const iface of interfaces) {
|
|
489
596
|
const interfaceName = iface.getName();
|
|
597
|
+
if (ignoreTypes.length > 0 && matchesPattern(interfaceName, ignoreTypes)) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
490
600
|
for (const prop of iface.getProperties()) {
|
|
601
|
+
const propertyName = prop.getName();
|
|
602
|
+
if (ignoreProperties.length > 0 && matchesPattern(propertyName, ignoreProperties)) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
491
605
|
const usage = isPropertyUnused(prop, isTestFile, project);
|
|
492
606
|
if (usage.isUnusedOrTestOnly) {
|
|
493
607
|
const relativePath = path5.relative(tsConfigDir, sourceFile.getFilePath());
|
|
@@ -498,7 +612,6 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
|
|
|
498
612
|
} else if (usage.onlyUsedInTests) {
|
|
499
613
|
severity = "info";
|
|
500
614
|
}
|
|
501
|
-
const propertyName = prop.getName();
|
|
502
615
|
const startPos = prop.getStart();
|
|
503
616
|
const lineStartPos = prop.getStartLinePos();
|
|
504
617
|
const character = startPos - lineStartPos + 1;
|
|
@@ -523,10 +636,15 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
|
|
|
523
636
|
// src/analyzeTypeAliases.ts
|
|
524
637
|
import path6 from "node:path";
|
|
525
638
|
import { Node as Node2 } from "ts-morph";
|
|
526
|
-
function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
639
|
+
function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
640
|
+
const { ignoreProperties = [] } = options;
|
|
527
641
|
if (!Node2.isPropertySignature(member)) {
|
|
528
642
|
return;
|
|
529
643
|
}
|
|
644
|
+
const propertyName = member.getName();
|
|
645
|
+
if (ignoreProperties.length > 0 && matchesPattern(propertyName, ignoreProperties)) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
530
648
|
const usage = isPropertyUnused(member, isTestFile, project);
|
|
531
649
|
if (!usage.isUnusedOrTestOnly) {
|
|
532
650
|
return;
|
|
@@ -539,7 +657,6 @@ function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isT
|
|
|
539
657
|
} else if (usage.onlyUsedInTests) {
|
|
540
658
|
severity = "info";
|
|
541
659
|
}
|
|
542
|
-
const propertyName = member.getName();
|
|
543
660
|
const startPos = member.getStart();
|
|
544
661
|
const lineStartPos = member.getStartLinePos();
|
|
545
662
|
const character = startPos - lineStartPos + 1;
|
|
@@ -557,8 +674,12 @@ function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isT
|
|
|
557
674
|
};
|
|
558
675
|
results.push(result);
|
|
559
676
|
}
|
|
560
|
-
function analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
677
|
+
function analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
678
|
+
const { ignoreTypes = [] } = options;
|
|
561
679
|
const typeName = typeAlias.getName();
|
|
680
|
+
if (ignoreTypes.length > 0 && matchesPattern(typeName, ignoreTypes)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
562
683
|
const typeNode = typeAlias.getTypeNode();
|
|
563
684
|
if (!typeNode) {
|
|
564
685
|
return;
|
|
@@ -567,18 +688,19 @@ function analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, result
|
|
|
567
688
|
return;
|
|
568
689
|
}
|
|
569
690
|
for (const member of typeNode.getMembers()) {
|
|
570
|
-
analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project);
|
|
691
|
+
analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project, options);
|
|
571
692
|
}
|
|
572
693
|
}
|
|
573
|
-
function analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
694
|
+
function analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project, options = {}) {
|
|
574
695
|
const typeAliases = sourceFile.getTypeAliases();
|
|
575
696
|
for (const typeAlias of typeAliases) {
|
|
576
|
-
analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project);
|
|
697
|
+
analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project, options);
|
|
577
698
|
}
|
|
578
699
|
}
|
|
579
700
|
|
|
580
701
|
// src/findUnusedProperties.ts
|
|
581
|
-
function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
702
|
+
function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targetFilePath, options = {}) {
|
|
703
|
+
const { ignoreFilePatterns = [], ...analyzeOptions } = options;
|
|
582
704
|
const results = [];
|
|
583
705
|
for (const sourceFile of project.getSourceFiles()) {
|
|
584
706
|
if (isTestFile(sourceFile)) {
|
|
@@ -587,15 +709,19 @@ function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targ
|
|
|
587
709
|
if (hasNoCheck(sourceFile)) {
|
|
588
710
|
continue;
|
|
589
711
|
}
|
|
590
|
-
|
|
712
|
+
const filePath = sourceFile.getFilePath();
|
|
713
|
+
if (targetFilePath && filePath !== targetFilePath) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (ignoreFilePatterns.length > 0 && matchesFilePattern(filePath, ignoreFilePatterns)) {
|
|
591
717
|
continue;
|
|
592
718
|
}
|
|
593
719
|
if (onProgress) {
|
|
594
|
-
const relativePath = path7.relative(tsConfigDir,
|
|
720
|
+
const relativePath = path7.relative(tsConfigDir, filePath);
|
|
595
721
|
onProgress(relativePath);
|
|
596
722
|
}
|
|
597
|
-
analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project);
|
|
598
|
-
analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project);
|
|
723
|
+
analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project, analyzeOptions);
|
|
724
|
+
analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project, analyzeOptions);
|
|
599
725
|
}
|
|
600
726
|
return results;
|
|
601
727
|
}
|
|
@@ -606,9 +732,26 @@ function isTestFile(sourceFile) {
|
|
|
606
732
|
const filePath = sourceFile.getFilePath();
|
|
607
733
|
return filePath.includes("__tests__") || TEST_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
608
734
|
}
|
|
735
|
+
function createIsTestFile(patterns = defaultConfig.testFilePatterns) {
|
|
736
|
+
return (sourceFile) => {
|
|
737
|
+
const filePath = sourceFile.getFilePath();
|
|
738
|
+
return matchesFilePattern(filePath, patterns);
|
|
739
|
+
};
|
|
740
|
+
}
|
|
609
741
|
|
|
610
742
|
// src/analyzeProject.ts
|
|
611
|
-
function analyzeProject(tsConfigPath, onProgress, targetFilePath,
|
|
743
|
+
function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFileOrOptions) {
|
|
744
|
+
let options = {};
|
|
745
|
+
if (typeof isTestFileOrOptions === "function") {
|
|
746
|
+
options = { isTestFile: isTestFileOrOptions };
|
|
747
|
+
} else if (isTestFileOrOptions) {
|
|
748
|
+
options = isTestFileOrOptions;
|
|
749
|
+
}
|
|
750
|
+
const config = {
|
|
751
|
+
...defaultConfig,
|
|
752
|
+
...options.config
|
|
753
|
+
};
|
|
754
|
+
const isTestFile2 = options.isTestFile ? options.isTestFile : config.testFilePatterns ? createIsTestFile(config.testFilePatterns) : isTestFile;
|
|
612
755
|
const project = new Project({
|
|
613
756
|
tsConfigFilePath: tsConfigPath
|
|
614
757
|
});
|
|
@@ -624,6 +767,9 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
|
|
|
624
767
|
if (targetFilePath && sf.getFilePath() !== targetFilePath) {
|
|
625
768
|
return false;
|
|
626
769
|
}
|
|
770
|
+
if (config.ignoreFilePatterns.length > 0 && matchesFilePattern(sf.getFilePath(), config.ignoreFilePatterns)) {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
627
773
|
return true;
|
|
628
774
|
});
|
|
629
775
|
const totalFiles = filesToAnalyze.length;
|
|
@@ -636,33 +782,48 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
|
|
|
636
782
|
onProgress(currentFile, totalFiles, filePath);
|
|
637
783
|
}
|
|
638
784
|
} : undefined;
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
785
|
+
const exportOptions = {
|
|
786
|
+
ignoreFilePatterns: config.ignoreFilePatterns,
|
|
787
|
+
ignoreExports: config.ignoreExports,
|
|
788
|
+
ignoreModuleAugmentations: config.ignoreModuleAugmentations
|
|
789
|
+
};
|
|
790
|
+
const propertyOptions = {
|
|
791
|
+
ignoreFilePatterns: config.ignoreFilePatterns,
|
|
792
|
+
ignoreProperties: config.ignoreProperties,
|
|
793
|
+
ignoreTypes: config.ignoreTypes
|
|
794
|
+
};
|
|
795
|
+
const neverReturnedOptions = {
|
|
796
|
+
ignoreFilePatterns: config.ignoreFilePatterns
|
|
797
|
+
};
|
|
798
|
+
const unusedExports = config.analyzeExports ? findUnusedExports(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath, exportOptions) : [];
|
|
799
|
+
const unusedProperties = config.analyzeProperties ? findUnusedProperties(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath, propertyOptions) : [];
|
|
800
|
+
const neverReturnedTypes = config.analyzeNeverReturnedTypes ? findNeverReturnedTypes(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath, neverReturnedOptions) : [];
|
|
642
801
|
const unusedFiles = [];
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
802
|
+
if (config.detectUnusedFiles) {
|
|
803
|
+
const fileExportCounts = new Map;
|
|
804
|
+
for (const sourceFile of filesToAnalyze) {
|
|
805
|
+
const filePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
|
|
806
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
807
|
+
const totalExports = exports.size;
|
|
808
|
+
if (totalExports > 0) {
|
|
809
|
+
fileExportCounts.set(filePath, { total: totalExports, unused: 0, testOnly: 0 });
|
|
810
|
+
}
|
|
650
811
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
812
|
+
for (const unusedExport of unusedExports) {
|
|
813
|
+
const counts = fileExportCounts.get(unusedExport.filePath);
|
|
814
|
+
if (counts) {
|
|
815
|
+
counts.unused++;
|
|
816
|
+
if (unusedExport.onlyUsedInTests) {
|
|
817
|
+
counts.testOnly++;
|
|
818
|
+
}
|
|
658
819
|
}
|
|
659
820
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
821
|
+
for (const [filePath, counts] of fileExportCounts.entries()) {
|
|
822
|
+
const allExportsUnused = counts.total > 0 && counts.unused === counts.total;
|
|
823
|
+
const hasAnyTestOnlyExports = counts.testOnly > 0;
|
|
824
|
+
if (allExportsUnused && !hasAnyTestOnlyExports) {
|
|
825
|
+
unusedFiles.push(filePath);
|
|
826
|
+
}
|
|
666
827
|
}
|
|
667
828
|
}
|
|
668
829
|
const results = {
|
|
@@ -1250,34 +1411,78 @@ function groupByFile(items) {
|
|
|
1250
1411
|
return grouped;
|
|
1251
1412
|
}
|
|
1252
1413
|
|
|
1414
|
+
// src/loadConfig.ts
|
|
1415
|
+
import fs2 from "node:fs";
|
|
1416
|
+
import path12 from "node:path";
|
|
1417
|
+
var CONFIG_FILE_NAME = "unused.config.ts";
|
|
1418
|
+
function loadConfigSync(targetDir, configPath) {
|
|
1419
|
+
const resolvedConfigPath = configPath ? path12.resolve(configPath) : path12.join(targetDir, CONFIG_FILE_NAME);
|
|
1420
|
+
if (!fs2.existsSync(resolvedConfigPath)) {
|
|
1421
|
+
return { ...defaultConfig };
|
|
1422
|
+
}
|
|
1423
|
+
try {
|
|
1424
|
+
const configModule = __require(resolvedConfigPath);
|
|
1425
|
+
const userConfig = configModule.default ?? configModule;
|
|
1426
|
+
return mergeConfig(userConfig);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1429
|
+
throw new Error(`Failed to load config from ${resolvedConfigPath}: ${errorMessage}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1253
1433
|
// src/cli.ts
|
|
1434
|
+
function parseArgs(args) {
|
|
1435
|
+
let command = "check";
|
|
1436
|
+
let tsConfigArg = "";
|
|
1437
|
+
let configPath;
|
|
1438
|
+
const positionalArgs = [];
|
|
1439
|
+
for (let i = 0;i < args.length; i++) {
|
|
1440
|
+
const arg = args[i] ?? "";
|
|
1441
|
+
if (arg === "--config" || arg === "-c") {
|
|
1442
|
+
configPath = args[++i];
|
|
1443
|
+
} else if (arg.startsWith("--config=")) {
|
|
1444
|
+
configPath = arg.slice("--config=".length);
|
|
1445
|
+
} else if (arg === "check" || arg === "fix") {
|
|
1446
|
+
command = arg;
|
|
1447
|
+
} else {
|
|
1448
|
+
positionalArgs.push(arg);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
tsConfigArg = positionalArgs[0] ?? "";
|
|
1452
|
+
const targetFilePath = positionalArgs[1];
|
|
1453
|
+
let tsConfigPath = path13.resolve(tsConfigArg);
|
|
1454
|
+
if (fs3.existsSync(tsConfigPath) && fs3.statSync(tsConfigPath).isDirectory()) {
|
|
1455
|
+
tsConfigPath = path13.join(tsConfigPath, "tsconfig.json");
|
|
1456
|
+
}
|
|
1457
|
+
return {
|
|
1458
|
+
command,
|
|
1459
|
+
tsConfigPath,
|
|
1460
|
+
targetFilePath: targetFilePath ? path13.resolve(targetFilePath) : undefined,
|
|
1461
|
+
configPath
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1254
1464
|
function main() {
|
|
1255
1465
|
const args = process.argv.slice(2);
|
|
1256
1466
|
if (args.length === 0) {
|
|
1257
|
-
console.log("Usage: ts-unused <command> <path-to-tsconfig.json> [file-path-to-check]");
|
|
1467
|
+
console.log("Usage: ts-unused <command> <path-to-tsconfig.json> [file-path-to-check] [options]");
|
|
1258
1468
|
console.log("");
|
|
1259
1469
|
console.log("Commands:");
|
|
1260
1470
|
console.log(" check - Analyze and report unused exports/properties (default)");
|
|
1261
1471
|
console.log(" fix - Automatically remove unused exports/properties and delete unused files");
|
|
1262
1472
|
console.log("");
|
|
1473
|
+
console.log("Options:");
|
|
1474
|
+
console.log(" --config, -c <path> - Path to configuration file (default: unused.config.ts in project dir)");
|
|
1475
|
+
console.log("");
|
|
1263
1476
|
console.log("Examples:");
|
|
1264
1477
|
console.log(" ts-unused check tsconfig.json");
|
|
1265
1478
|
console.log(" ts-unused check ./project-dir # looks for tsconfig.json inside");
|
|
1266
1479
|
console.log(" ts-unused fix tsconfig.json");
|
|
1480
|
+
console.log(" ts-unused check tsconfig.json --config ./unused.config.ts");
|
|
1267
1481
|
process.exit(1);
|
|
1268
1482
|
}
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (firstArg === "check" || firstArg === "fix") {
|
|
1273
|
-
command = firstArg;
|
|
1274
|
-
configIndex = 1;
|
|
1275
|
-
}
|
|
1276
|
-
let tsConfigPath = path12.resolve(args[configIndex] ?? "");
|
|
1277
|
-
if (fs2.existsSync(tsConfigPath) && fs2.statSync(tsConfigPath).isDirectory()) {
|
|
1278
|
-
tsConfigPath = path12.join(tsConfigPath, "tsconfig.json");
|
|
1279
|
-
}
|
|
1280
|
-
const targetFilePath = args[configIndex + 1] ? path12.resolve(args[configIndex + 1]) : undefined;
|
|
1483
|
+
const { command, tsConfigPath, targetFilePath, configPath } = parseArgs(args);
|
|
1484
|
+
const tsConfigDir = path13.dirname(path13.resolve(tsConfigPath));
|
|
1485
|
+
const config = loadConfigSync(tsConfigDir, configPath);
|
|
1281
1486
|
if (command === "fix") {
|
|
1282
1487
|
console.log(`Fixing TypeScript project: ${tsConfigPath}`);
|
|
1283
1488
|
console.log("");
|
|
@@ -1316,11 +1521,10 @@ function main() {
|
|
|
1316
1521
|
const filledLength = Math.min(barLength, Math.max(0, Math.floor(current / total * barLength)));
|
|
1317
1522
|
const emptyLength = Math.max(0, barLength - filledLength);
|
|
1318
1523
|
const bar = "█".repeat(filledLength) + "░".repeat(emptyLength);
|
|
1319
|
-
const fileName =
|
|
1524
|
+
const fileName = path13.basename(filePath);
|
|
1320
1525
|
process.stdout.write(`\r\x1B[KProgress: [${bar}] ${percentage}% (${current}/${total}) ${fileName}`);
|
|
1321
|
-
}, targetFilePath);
|
|
1526
|
+
}, targetFilePath, { config });
|
|
1322
1527
|
process.stdout.write("\r\x1B[K");
|
|
1323
|
-
const tsConfigDir = path12.dirname(path12.resolve(tsConfigPath));
|
|
1324
1528
|
const output = formatResults(results, tsConfigDir);
|
|
1325
1529
|
console.log(output);
|
|
1326
1530
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for ts-unused analysis.
|
|
3
|
+
*/
|
|
4
|
+
export interface UnusedConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Patterns to identify test files. Supports glob patterns.
|
|
7
|
+
* Files matching these patterns will be treated as test files
|
|
8
|
+
* and their usages won't count as production usage.
|
|
9
|
+
*
|
|
10
|
+
* @default ["**\/*.test.ts", "**\/*.test.tsx", "**\/*.spec.ts", "**\/*.spec.tsx", "**\/__tests__/**"]
|
|
11
|
+
*/
|
|
12
|
+
testFilePatterns?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Patterns for files to completely ignore during analysis.
|
|
15
|
+
* Supports glob patterns.
|
|
16
|
+
*
|
|
17
|
+
* @default []
|
|
18
|
+
*/
|
|
19
|
+
ignoreFilePatterns?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Export names to ignore (will not be reported as unused).
|
|
22
|
+
* Supports exact names or glob patterns.
|
|
23
|
+
*
|
|
24
|
+
* @default []
|
|
25
|
+
*/
|
|
26
|
+
ignoreExports?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Property names to ignore (will not be reported as unused).
|
|
29
|
+
* Supports exact names or glob patterns.
|
|
30
|
+
*
|
|
31
|
+
* @default []
|
|
32
|
+
*/
|
|
33
|
+
ignoreProperties?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Type names to ignore (their properties will not be analyzed).
|
|
36
|
+
* Supports exact names or glob patterns.
|
|
37
|
+
*
|
|
38
|
+
* @default []
|
|
39
|
+
*/
|
|
40
|
+
ignoreTypes?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Whether to ignore module augmentation declarations.
|
|
43
|
+
* Module augmentations (declare module "...") are typically used
|
|
44
|
+
* to extend existing types and don't have direct usages.
|
|
45
|
+
*
|
|
46
|
+
* @default true
|
|
47
|
+
*/
|
|
48
|
+
ignoreModuleAugmentations?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to analyze properties for unused status.
|
|
51
|
+
*
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
analyzeProperties?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether to analyze exports for unused status.
|
|
57
|
+
*
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
analyzeExports?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Whether to analyze for never-returned types in function return types.
|
|
63
|
+
*
|
|
64
|
+
* @default true
|
|
65
|
+
*/
|
|
66
|
+
analyzeNeverReturnedTypes?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to detect completely unused files.
|
|
69
|
+
*
|
|
70
|
+
* @default true
|
|
71
|
+
*/
|
|
72
|
+
detectUnusedFiles?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Default configuration values.
|
|
76
|
+
*/
|
|
77
|
+
export declare const defaultConfig: Required<UnusedConfig>;
|
|
78
|
+
/**
|
|
79
|
+
* Helper function for creating a strongly-typed configuration file.
|
|
80
|
+
* Use this in your `unused.config.ts` file:
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* import { defineConfig } from "ts-unused";
|
|
85
|
+
*
|
|
86
|
+
* export default defineConfig({
|
|
87
|
+
* testFilePatterns: ["**\/*.test.ts", "**\/TestLogger.ts"],
|
|
88
|
+
* ignoreExports: ["moduleAugmentation"],
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function defineConfig(config: UnusedConfig): UnusedConfig;
|
|
93
|
+
/**
|
|
94
|
+
* Merges user config with default config.
|
|
95
|
+
*/
|
|
96
|
+
export declare function mergeConfig(userConfig: UnusedConfig): Required<UnusedConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values.
|
|
3
|
+
*/
|
|
4
|
+
export const defaultConfig = {
|
|
5
|
+
testFilePatterns: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "**/__tests__/**"],
|
|
6
|
+
ignoreFilePatterns: [],
|
|
7
|
+
ignoreExports: [],
|
|
8
|
+
ignoreProperties: [],
|
|
9
|
+
ignoreTypes: [],
|
|
10
|
+
ignoreModuleAugmentations: true,
|
|
11
|
+
analyzeProperties: true,
|
|
12
|
+
analyzeExports: true,
|
|
13
|
+
analyzeNeverReturnedTypes: true,
|
|
14
|
+
detectUnusedFiles: true,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Helper function for creating a strongly-typed configuration file.
|
|
18
|
+
* Use this in your `unused.config.ts` file:
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { defineConfig } from "ts-unused";
|
|
23
|
+
*
|
|
24
|
+
* export default defineConfig({
|
|
25
|
+
* testFilePatterns: ["**\/*.test.ts", "**\/TestLogger.ts"],
|
|
26
|
+
* ignoreExports: ["moduleAugmentation"],
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function defineConfig(config) {
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Merges user config with default config.
|
|
35
|
+
*/
|
|
36
|
+
export function mergeConfig(userConfig) {
|
|
37
|
+
const result = {
|
|
38
|
+
testFilePatterns: userConfig.testFilePatterns ?? defaultConfig.testFilePatterns,
|
|
39
|
+
ignoreFilePatterns: userConfig.ignoreFilePatterns ?? defaultConfig.ignoreFilePatterns,
|
|
40
|
+
ignoreExports: userConfig.ignoreExports ?? defaultConfig.ignoreExports,
|
|
41
|
+
ignoreProperties: userConfig.ignoreProperties ?? defaultConfig.ignoreProperties,
|
|
42
|
+
ignoreTypes: userConfig.ignoreTypes ?? defaultConfig.ignoreTypes,
|
|
43
|
+
ignoreModuleAugmentations: userConfig.ignoreModuleAugmentations ?? defaultConfig.ignoreModuleAugmentations,
|
|
44
|
+
analyzeProperties: userConfig.analyzeProperties ?? defaultConfig.analyzeProperties,
|
|
45
|
+
analyzeExports: userConfig.analyzeExports ?? defaultConfig.analyzeExports,
|
|
46
|
+
analyzeNeverReturnedTypes: userConfig.analyzeNeverReturnedTypes ?? defaultConfig.analyzeNeverReturnedTypes,
|
|
47
|
+
detectUnusedFiles: userConfig.detectUnusedFiles ?? defaultConfig.detectUnusedFiles,
|
|
48
|
+
};
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PropertyDeclaration, PropertySignature } from "ts-morph";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts TODO comment text from the leading comments of a property.
|
|
4
|
+
* Returns undefined if no TODO comment is found.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractTodoComment(prop: PropertySignature | PropertyDeclaration): string | undefined;
|