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.
Files changed (45) hide show
  1. package/README.md +271 -1
  2. package/dist/analyzeFunctionReturnTypes.d.ts +6 -0
  3. package/dist/analyzeFunctionReturnTypes.js +235 -0
  4. package/dist/analyzeInterfaces.d.ts +7 -0
  5. package/dist/analyzeInterfaces.js +51 -0
  6. package/dist/analyzeProject.d.ts +13 -0
  7. package/dist/analyzeProject.js +131 -0
  8. package/dist/analyzeTypeAliases.d.ts +9 -0
  9. package/dist/analyzeTypeAliases.js +70 -0
  10. package/dist/checkExportUsage.d.ts +7 -0
  11. package/dist/checkExportUsage.js +126 -0
  12. package/dist/checkGitStatus.d.ts +7 -0
  13. package/dist/checkGitStatus.js +49 -0
  14. package/dist/cli.js +267 -63
  15. package/dist/config.d.ts +96 -0
  16. package/dist/config.js +50 -0
  17. package/dist/extractTodoComment.d.ts +6 -0
  18. package/dist/extractTodoComment.js +27 -0
  19. package/dist/findNeverReturnedTypes.d.ts +6 -0
  20. package/dist/findNeverReturnedTypes.js +36 -0
  21. package/dist/findStructurallyEquivalentProperties.d.ts +12 -0
  22. package/dist/findStructurallyEquivalentProperties.js +60 -0
  23. package/dist/findUnusedExports.d.ts +7 -0
  24. package/dist/findUnusedExports.js +36 -0
  25. package/dist/findUnusedProperties.d.ts +7 -0
  26. package/dist/findUnusedProperties.js +32 -0
  27. package/dist/fixProject.d.ts +13 -0
  28. package/dist/fixProject.js +549 -0
  29. package/dist/formatResults.d.ts +2 -0
  30. package/dist/formatResults.js +112 -0
  31. package/dist/hasNoCheck.d.ts +2 -0
  32. package/dist/hasNoCheck.js +8 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +9 -0
  35. package/dist/isPropertyUnused.d.ts +7 -0
  36. package/dist/isPropertyUnused.js +48 -0
  37. package/dist/isTestFile.d.ts +14 -0
  38. package/dist/isTestFile.js +23 -0
  39. package/dist/loadConfig.d.ts +20 -0
  40. package/dist/loadConfig.js +54 -0
  41. package/dist/patternMatcher.d.ts +26 -0
  42. package/dist/patternMatcher.js +83 -0
  43. package/dist/types.d.ts +40 -0
  44. package/dist/types.js +1 -0
  45. 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 fs2 from "node:fs";
5
- import path12 from "node:path";
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
- if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
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, sourceFile.getFilePath());
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 checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile) {
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
- if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
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, sourceFile.getFilePath());
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
- if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
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, sourceFile.getFilePath());
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, isTestFile2 = isTestFile) {
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 unusedExports = findUnusedExports(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
640
- const unusedProperties = findUnusedProperties(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
641
- const neverReturnedTypes = findNeverReturnedTypes(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
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
- const fileExportCounts = new Map;
644
- for (const sourceFile of filesToAnalyze) {
645
- const filePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
646
- const exports = sourceFile.getExportedDeclarations();
647
- const totalExports = exports.size;
648
- if (totalExports > 0) {
649
- fileExportCounts.set(filePath, { total: totalExports, unused: 0, testOnly: 0 });
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
- for (const unusedExport of unusedExports) {
653
- const counts = fileExportCounts.get(unusedExport.filePath);
654
- if (counts) {
655
- counts.unused++;
656
- if (unusedExport.onlyUsedInTests) {
657
- counts.testOnly++;
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
- for (const [filePath, counts] of fileExportCounts.entries()) {
662
- const allExportsUnused = counts.total > 0 && counts.unused === counts.total;
663
- const hasAnyTestOnlyExports = counts.testOnly > 0;
664
- if (allExportsUnused && !hasAnyTestOnlyExports) {
665
- unusedFiles.push(filePath);
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 firstArg = args[0] ?? "";
1270
- let command = "check";
1271
- let configIndex = 0;
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 = path12.basename(filePath);
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
  }
@@ -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;