@bfra.me/workspace-analyzer 0.1.0

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 (73) hide show
  1. package/README.md +402 -0
  2. package/lib/chunk-4LSFAAZW.js +1 -0
  3. package/lib/chunk-JDF7DQ4V.js +27 -0
  4. package/lib/chunk-WOJ4C7N7.js +7122 -0
  5. package/lib/cli.d.ts +1 -0
  6. package/lib/cli.js +318 -0
  7. package/lib/index.d.ts +3701 -0
  8. package/lib/index.js +1262 -0
  9. package/lib/types/index.d.ts +146 -0
  10. package/lib/types/index.js +28 -0
  11. package/package.json +89 -0
  12. package/src/analyzers/analyzer.ts +201 -0
  13. package/src/analyzers/architectural-analyzer.ts +304 -0
  14. package/src/analyzers/build-config-analyzer.ts +334 -0
  15. package/src/analyzers/circular-import-analyzer.ts +463 -0
  16. package/src/analyzers/config-consistency-analyzer.ts +335 -0
  17. package/src/analyzers/dead-code-analyzer.ts +565 -0
  18. package/src/analyzers/duplicate-code-analyzer.ts +626 -0
  19. package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
  20. package/src/analyzers/eslint-config-analyzer.ts +281 -0
  21. package/src/analyzers/exports-field-analyzer.ts +324 -0
  22. package/src/analyzers/index.ts +388 -0
  23. package/src/analyzers/large-dependency-analyzer.ts +535 -0
  24. package/src/analyzers/package-json-analyzer.ts +349 -0
  25. package/src/analyzers/peer-dependency-analyzer.ts +275 -0
  26. package/src/analyzers/tree-shaking-analyzer.ts +623 -0
  27. package/src/analyzers/tsconfig-analyzer.ts +382 -0
  28. package/src/analyzers/unused-dependency-analyzer.ts +356 -0
  29. package/src/analyzers/version-alignment-analyzer.ts +308 -0
  30. package/src/api/analyze-workspace.ts +245 -0
  31. package/src/api/index.ts +11 -0
  32. package/src/cache/cache-manager.ts +495 -0
  33. package/src/cache/cache-schema.ts +247 -0
  34. package/src/cache/change-detector.ts +169 -0
  35. package/src/cache/file-hasher.ts +65 -0
  36. package/src/cache/index.ts +47 -0
  37. package/src/cli/commands/analyze.ts +240 -0
  38. package/src/cli/commands/index.ts +5 -0
  39. package/src/cli/index.ts +61 -0
  40. package/src/cli/types.ts +65 -0
  41. package/src/cli/ui.ts +213 -0
  42. package/src/cli.ts +9 -0
  43. package/src/config/defaults.ts +183 -0
  44. package/src/config/index.ts +81 -0
  45. package/src/config/loader.ts +270 -0
  46. package/src/config/merger.ts +229 -0
  47. package/src/config/schema.ts +263 -0
  48. package/src/core/incremental-analyzer.ts +462 -0
  49. package/src/core/index.ts +34 -0
  50. package/src/core/orchestrator.ts +416 -0
  51. package/src/graph/dependency-graph.ts +408 -0
  52. package/src/graph/index.ts +19 -0
  53. package/src/index.ts +417 -0
  54. package/src/parser/config-parser.ts +491 -0
  55. package/src/parser/import-extractor.ts +340 -0
  56. package/src/parser/index.ts +54 -0
  57. package/src/parser/typescript-parser.ts +95 -0
  58. package/src/performance/bundle-estimator.ts +444 -0
  59. package/src/performance/index.ts +27 -0
  60. package/src/reporters/console-reporter.ts +355 -0
  61. package/src/reporters/index.ts +49 -0
  62. package/src/reporters/json-reporter.ts +273 -0
  63. package/src/reporters/markdown-reporter.ts +349 -0
  64. package/src/reporters/reporter.ts +399 -0
  65. package/src/rules/builtin-rules.ts +709 -0
  66. package/src/rules/index.ts +52 -0
  67. package/src/rules/rule-engine.ts +409 -0
  68. package/src/scanner/index.ts +18 -0
  69. package/src/scanner/workspace-scanner.ts +403 -0
  70. package/src/types/index.ts +176 -0
  71. package/src/types/result.ts +19 -0
  72. package/src/utils/index.ts +7 -0
  73. package/src/utils/pattern-matcher.ts +48 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * DeadCodeAnalyzer - Detects unreachable exports and unused code.
3
+ *
4
+ * Identifies code that is exported but never imported or used:
5
+ * - Exported functions never imported anywhere
6
+ * - Exported classes never instantiated or extended
7
+ * - Exported constants never referenced
8
+ * - Public API surface that could be reduced
9
+ *
10
+ * This helps identify code that can be safely removed to reduce bundle size
11
+ * and maintenance burden.
12
+ */
13
+
14
+ import type {SourceFile} from 'ts-morph'
15
+
16
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
17
+ import type {Issue, IssueLocation, Severity} from '../types/index'
18
+ import type {Result} from '../types/result'
19
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
20
+
21
+ import {createProject} from '@bfra.me/doc-sync/parsers'
22
+ import {ok} from '@bfra.me/es/result'
23
+ import {SyntaxKind} from 'ts-morph'
24
+
25
+ import {extractImports} from '../parser/import-extractor'
26
+ import {createIssue, filterIssues} from './analyzer'
27
+
28
+ /**
29
+ * Configuration options for DeadCodeAnalyzer.
30
+ */
31
+ export interface DeadCodeAnalyzerOptions {
32
+ /** Check for unused exports */
33
+ readonly checkUnusedExports?: boolean
34
+ /** Check for unreachable code paths */
35
+ readonly checkUnreachableCode?: boolean
36
+ /** Analyze cross-package usage (exports used in other workspace packages) */
37
+ readonly crossPackageAnalysis?: boolean
38
+ /** Consider re-exports as usage */
39
+ readonly countReexportsAsUsage?: boolean
40
+ /** Export patterns to ignore (e.g., 'index.ts' entry points) */
41
+ readonly ignoreExportPatterns?: readonly string[]
42
+ /** Severity for unused export warnings */
43
+ readonly unusedExportSeverity?: Severity
44
+ /** Severity for unreachable code warnings */
45
+ readonly unreachableCodeSeverity?: Severity
46
+ /** File patterns to exclude from analysis */
47
+ readonly excludePatterns?: readonly string[]
48
+ }
49
+
50
+ const DEFAULT_OPTIONS: Required<DeadCodeAnalyzerOptions> = {
51
+ checkUnusedExports: true,
52
+ checkUnreachableCode: true,
53
+ crossPackageAnalysis: true,
54
+ countReexportsAsUsage: true,
55
+ ignoreExportPatterns: ['**/index.ts', '**/index.tsx'],
56
+ unusedExportSeverity: 'info',
57
+ unreachableCodeSeverity: 'warning',
58
+ excludePatterns: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**', '**/__mocks__/**'],
59
+ }
60
+
61
+ export const deadCodeAnalyzerMetadata: AnalyzerMetadata = {
62
+ id: 'dead-code',
63
+ name: 'Dead Code Analyzer',
64
+ description: 'Detects unreachable exports and unused code that can be safely removed',
65
+ categories: ['unused-export', 'performance'],
66
+ defaultSeverity: 'info',
67
+ }
68
+
69
+ /**
70
+ * Information about an exported symbol.
71
+ */
72
+ export interface ExportedSymbol {
73
+ /** Symbol name */
74
+ readonly name: string
75
+ /** Export type */
76
+ readonly type: 'function' | 'class' | 'variable' | 'type' | 'interface' | 'enum' | 'namespace'
77
+ /** File where exported */
78
+ readonly filePath: string
79
+ /** Location of the export */
80
+ readonly location: IssueLocation
81
+ /** Whether this is a re-export */
82
+ readonly isReexport: boolean
83
+ /** Whether this is a default export */
84
+ readonly isDefault: boolean
85
+ /** Package containing this export */
86
+ readonly packageName: string
87
+ }
88
+
89
+ /**
90
+ * Creates a DeadCodeAnalyzer instance.
91
+ */
92
+ export function createDeadCodeAnalyzer(options: DeadCodeAnalyzerOptions = {}): Analyzer {
93
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
94
+
95
+ return {
96
+ metadata: deadCodeAnalyzerMetadata,
97
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
98
+ const issues: Issue[] = []
99
+
100
+ // Step 1: Collect all exports from all packages
101
+ context.reportProgress?.('Collecting exports...')
102
+ const allExports = new Map<string, ExportedSymbol[]>()
103
+
104
+ for (const pkg of context.packages) {
105
+ const exports = await collectPackageExports(pkg, resolvedOptions)
106
+ allExports.set(pkg.name, exports)
107
+ }
108
+
109
+ // Step 2: Collect all imports from all packages
110
+ context.reportProgress?.('Collecting imports...')
111
+ const allImports = new Map<string, ImportInfo[]>()
112
+
113
+ for (const pkg of context.packages) {
114
+ const imports = await collectPackageImports(pkg, resolvedOptions)
115
+ allImports.set(pkg.name, imports)
116
+ }
117
+
118
+ // Step 3: Analyze export usage
119
+ context.reportProgress?.('Analyzing export usage...')
120
+
121
+ for (const pkg of context.packages) {
122
+ const packageIssues = analyzePackageDeadCode(pkg, allExports, allImports, resolvedOptions)
123
+ issues.push(...packageIssues)
124
+ }
125
+
126
+ return ok(filterIssues(issues, context.config))
127
+ },
128
+ }
129
+ }
130
+
131
+ interface ImportInfo {
132
+ /** Module specifier */
133
+ readonly moduleSpecifier: string
134
+ /** Imported names */
135
+ readonly importedNames: readonly string[]
136
+ /** File where the import is */
137
+ readonly filePath: string
138
+ /** Package name */
139
+ readonly packageName: string
140
+ }
141
+
142
+ async function collectPackageExports(
143
+ pkg: WorkspacePackage,
144
+ options: Required<DeadCodeAnalyzerOptions>,
145
+ ): Promise<ExportedSymbol[]> {
146
+ const exports: ExportedSymbol[] = []
147
+
148
+ const sourceFiles = filterSourceFiles(pkg.sourceFiles, options.excludePatterns)
149
+ if (sourceFiles.length === 0) {
150
+ return exports
151
+ }
152
+
153
+ const project = createProject()
154
+
155
+ for (const filePath of sourceFiles) {
156
+ // Skip index files if configured
157
+ if (shouldIgnoreFile(filePath, options.ignoreExportPatterns)) {
158
+ continue
159
+ }
160
+
161
+ try {
162
+ const sourceFile = project.addSourceFileAtPath(filePath)
163
+ const fileExports = collectFileExports(sourceFile, pkg, filePath)
164
+ exports.push(...fileExports)
165
+ } catch {
166
+ // File may not be parseable
167
+ }
168
+ }
169
+
170
+ return exports
171
+ }
172
+
173
+ function collectFileExports(
174
+ sourceFile: SourceFile,
175
+ pkg: WorkspacePackage,
176
+ filePath: string,
177
+ ): ExportedSymbol[] {
178
+ const exports: ExportedSymbol[] = []
179
+
180
+ // Collect exported declarations
181
+ for (const stmt of sourceFile.getStatements()) {
182
+ // Function declarations
183
+ if (stmt.getKind() === SyntaxKind.FunctionDeclaration) {
184
+ const funcDecl = stmt.asKind(SyntaxKind.FunctionDeclaration)
185
+ if (funcDecl === undefined) continue
186
+
187
+ if (funcDecl.isExported()) {
188
+ const name = funcDecl.getName()
189
+ if (name === undefined) continue
190
+
191
+ const {line, column} = sourceFile.getLineAndColumnAtPos(funcDecl.getStart())
192
+
193
+ exports.push({
194
+ name,
195
+ type: 'function',
196
+ filePath,
197
+ location: {filePath, line, column},
198
+ isReexport: false,
199
+ isDefault: funcDecl.isDefaultExport(),
200
+ packageName: pkg.name,
201
+ })
202
+ }
203
+ }
204
+
205
+ // Class declarations
206
+ if (stmt.getKind() === SyntaxKind.ClassDeclaration) {
207
+ const classDecl = stmt.asKind(SyntaxKind.ClassDeclaration)
208
+ if (classDecl === undefined) continue
209
+
210
+ if (classDecl.isExported()) {
211
+ const name = classDecl.getName()
212
+ if (name === undefined) continue
213
+
214
+ const {line, column} = sourceFile.getLineAndColumnAtPos(classDecl.getStart())
215
+
216
+ exports.push({
217
+ name,
218
+ type: 'class',
219
+ filePath,
220
+ location: {filePath, line, column},
221
+ isReexport: false,
222
+ isDefault: classDecl.isDefaultExport(),
223
+ packageName: pkg.name,
224
+ })
225
+ }
226
+ }
227
+
228
+ // Variable statements
229
+ if (stmt.getKind() === SyntaxKind.VariableStatement) {
230
+ const varStmt = stmt.asKind(SyntaxKind.VariableStatement)
231
+ if (varStmt === undefined) continue
232
+
233
+ if (varStmt.isExported()) {
234
+ for (const decl of varStmt.getDeclarations()) {
235
+ const name = decl.getName()
236
+ const {line, column} = sourceFile.getLineAndColumnAtPos(decl.getStart())
237
+
238
+ exports.push({
239
+ name,
240
+ type: 'variable',
241
+ filePath,
242
+ location: {filePath, line, column},
243
+ isReexport: false,
244
+ isDefault: false,
245
+ packageName: pkg.name,
246
+ })
247
+ }
248
+ }
249
+ }
250
+
251
+ // Type aliases
252
+ if (stmt.getKind() === SyntaxKind.TypeAliasDeclaration) {
253
+ const typeDecl = stmt.asKind(SyntaxKind.TypeAliasDeclaration)
254
+ if (typeDecl === undefined) continue
255
+
256
+ if (typeDecl.isExported()) {
257
+ const name = typeDecl.getName()
258
+ const {line, column} = sourceFile.getLineAndColumnAtPos(typeDecl.getStart())
259
+
260
+ exports.push({
261
+ name,
262
+ type: 'type',
263
+ filePath,
264
+ location: {filePath, line, column},
265
+ isReexport: false,
266
+ isDefault: false,
267
+ packageName: pkg.name,
268
+ })
269
+ }
270
+ }
271
+
272
+ // Interfaces
273
+ if (stmt.getKind() === SyntaxKind.InterfaceDeclaration) {
274
+ const ifaceDecl = stmt.asKind(SyntaxKind.InterfaceDeclaration)
275
+ if (ifaceDecl === undefined) continue
276
+
277
+ if (ifaceDecl.isExported()) {
278
+ const name = ifaceDecl.getName()
279
+ const {line, column} = sourceFile.getLineAndColumnAtPos(ifaceDecl.getStart())
280
+
281
+ exports.push({
282
+ name,
283
+ type: 'interface',
284
+ filePath,
285
+ location: {filePath, line, column},
286
+ isReexport: false,
287
+ isDefault: false,
288
+ packageName: pkg.name,
289
+ })
290
+ }
291
+ }
292
+
293
+ // Enums
294
+ if (stmt.getKind() === SyntaxKind.EnumDeclaration) {
295
+ const enumDecl = stmt.asKind(SyntaxKind.EnumDeclaration)
296
+ if (enumDecl === undefined) continue
297
+
298
+ if (enumDecl.isExported()) {
299
+ const name = enumDecl.getName()
300
+ const {line, column} = sourceFile.getLineAndColumnAtPos(enumDecl.getStart())
301
+
302
+ exports.push({
303
+ name,
304
+ type: 'enum',
305
+ filePath,
306
+ location: {filePath, line, column},
307
+ isReexport: false,
308
+ isDefault: false,
309
+ packageName: pkg.name,
310
+ })
311
+ }
312
+ }
313
+ }
314
+
315
+ // Collect re-exports from export declarations
316
+ for (const exportDecl of sourceFile.getExportDeclarations()) {
317
+ const moduleSpecifier = exportDecl.getModuleSpecifierValue()
318
+ if (moduleSpecifier === undefined) continue
319
+
320
+ const {line, column} = sourceFile.getLineAndColumnAtPos(exportDecl.getStart())
321
+
322
+ // Named re-exports
323
+ for (const namedExport of exportDecl.getNamedExports()) {
324
+ const name = namedExport.getName()
325
+ exports.push({
326
+ name,
327
+ type: 'variable', // Type is unknown from re-export
328
+ filePath,
329
+ location: {filePath, line, column},
330
+ isReexport: true,
331
+ isDefault: false,
332
+ packageName: pkg.name,
333
+ })
334
+ }
335
+ }
336
+
337
+ return exports
338
+ }
339
+
340
+ async function collectPackageImports(
341
+ pkg: WorkspacePackage,
342
+ options: Required<DeadCodeAnalyzerOptions>,
343
+ ): Promise<ImportInfo[]> {
344
+ const imports: ImportInfo[] = []
345
+
346
+ const sourceFiles = filterSourceFiles(pkg.sourceFiles, options.excludePatterns)
347
+ if (sourceFiles.length === 0) {
348
+ return imports
349
+ }
350
+
351
+ const project = createProject()
352
+
353
+ for (const filePath of sourceFiles) {
354
+ try {
355
+ const sourceFile = project.addSourceFileAtPath(filePath)
356
+ const result = extractImports(sourceFile)
357
+
358
+ for (const imp of result.imports) {
359
+ const importedNames: string[] = []
360
+
361
+ if (imp.defaultImport !== undefined) {
362
+ importedNames.push('default')
363
+ }
364
+ if (imp.namespaceImport !== undefined) {
365
+ importedNames.push('*')
366
+ }
367
+ if (imp.importedNames !== undefined) {
368
+ importedNames.push(...imp.importedNames)
369
+ }
370
+
371
+ imports.push({
372
+ moduleSpecifier: imp.moduleSpecifier,
373
+ importedNames,
374
+ filePath,
375
+ packageName: pkg.name,
376
+ })
377
+ }
378
+ } catch {
379
+ // File may not be parseable
380
+ }
381
+ }
382
+
383
+ return imports
384
+ }
385
+
386
+ function analyzePackageDeadCode(
387
+ pkg: WorkspacePackage,
388
+ allExports: Map<string, ExportedSymbol[]>,
389
+ allImports: Map<string, ImportInfo[]>,
390
+ options: Required<DeadCodeAnalyzerOptions>,
391
+ ): Issue[] {
392
+ const issues: Issue[] = []
393
+
394
+ const packageExports = allExports.get(pkg.name) ?? []
395
+ if (packageExports.length === 0) {
396
+ return issues
397
+ }
398
+
399
+ // Build a set of all imported symbols across the workspace
400
+ const usedSymbols = new Set<string>()
401
+
402
+ for (const [pkgName, imports] of allImports) {
403
+ // Skip same-package imports if not doing cross-package analysis
404
+ if (!options.crossPackageAnalysis && pkgName !== pkg.name) {
405
+ continue
406
+ }
407
+
408
+ for (const imp of imports) {
409
+ // Check if this import refers to the current package
410
+ const isCurrentPackage =
411
+ imp.moduleSpecifier.startsWith(`.`) ||
412
+ imp.moduleSpecifier === pkg.name ||
413
+ imp.moduleSpecifier.startsWith(`${pkg.name}/`)
414
+
415
+ if (isCurrentPackage) {
416
+ for (const name of imp.importedNames) {
417
+ usedSymbols.add(name)
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ // Also consider exports from index files as "using" internal exports
424
+ if (options.countReexportsAsUsage) {
425
+ for (const exp of packageExports) {
426
+ if (exp.isReexport) {
427
+ // The re-exported symbol is considered used
428
+ usedSymbols.add(exp.name)
429
+ }
430
+ }
431
+ }
432
+
433
+ // Check each export for usage
434
+ for (const exp of packageExports) {
435
+ // Skip re-exports (they're tracked separately)
436
+ if (exp.isReexport) continue
437
+
438
+ // Skip types and interfaces (they're erased at runtime)
439
+ if (exp.type === 'type' || exp.type === 'interface') continue
440
+
441
+ const isUsed = usedSymbols.has(exp.name) || (exp.isDefault && usedSymbols.has('default'))
442
+
443
+ if (!isUsed) {
444
+ issues.push(
445
+ createIssue({
446
+ id: 'unused-export',
447
+ title: `Unused export: '${exp.name}'`,
448
+ description:
449
+ `The ${exp.type} '${exp.name}' is exported but never imported within the workspace. ` +
450
+ `This may indicate dead code that can be removed to reduce bundle size.`,
451
+ severity: options.unusedExportSeverity,
452
+ category: 'unused-export',
453
+ location: exp.location,
454
+ suggestion:
455
+ `If '${exp.name}' is only used externally (outside this workspace), consider documenting it as public API. ` +
456
+ `Otherwise, consider removing the export or the entire declaration if unused.`,
457
+ metadata: {
458
+ packageName: pkg.name,
459
+ exportName: exp.name,
460
+ exportType: exp.type,
461
+ isDefault: exp.isDefault,
462
+ },
463
+ }),
464
+ )
465
+ }
466
+ }
467
+
468
+ return issues
469
+ }
470
+
471
+ function filterSourceFiles(
472
+ sourceFiles: readonly string[],
473
+ excludePatterns: readonly string[],
474
+ ): string[] {
475
+ return sourceFiles.filter(filePath => {
476
+ const fileName = filePath.split('/').pop() ?? ''
477
+
478
+ return !excludePatterns.some(pattern => {
479
+ if (pattern.includes('**')) {
480
+ const regex = patternToRegex(pattern)
481
+ return regex.test(filePath)
482
+ }
483
+ return fileName.includes(pattern.replaceAll('*', ''))
484
+ })
485
+ })
486
+ }
487
+
488
+ function shouldIgnoreFile(filePath: string, ignorePatterns: readonly string[]): boolean {
489
+ const fileName = filePath.split('/').pop() ?? ''
490
+
491
+ return ignorePatterns.some(pattern => {
492
+ if (pattern.includes('**')) {
493
+ const regex = patternToRegex(pattern)
494
+ return regex.test(filePath)
495
+ }
496
+ return (
497
+ fileName === pattern.replaceAll('*', '') ||
498
+ fileName.includes(pattern.replaceAll('**/', '').replaceAll('*', ''))
499
+ )
500
+ })
501
+ }
502
+
503
+ function patternToRegex(pattern: string): RegExp {
504
+ const escaped = pattern
505
+ .replaceAll('.', String.raw`\.`)
506
+ .replaceAll('**', '.*')
507
+ .replaceAll('*', '[^/]*')
508
+ return new RegExp(escaped)
509
+ }
510
+
511
+ /**
512
+ * Computes statistics about dead code in a package.
513
+ */
514
+ export interface DeadCodeStats {
515
+ /** Total exports analyzed */
516
+ readonly totalExports: number
517
+ /** Number of unused exports */
518
+ readonly unusedExports: number
519
+ /** Number of used exports */
520
+ readonly usedExports: number
521
+ /** Usage percentage */
522
+ readonly usagePercentage: number
523
+ /** Breakdown by export type */
524
+ readonly byType: Readonly<Record<string, {total: number; unused: number}>>
525
+ }
526
+
527
+ /**
528
+ * Computes dead code statistics from analysis results.
529
+ */
530
+ export function computeDeadCodeStats(
531
+ exports: readonly ExportedSymbol[],
532
+ unusedExportNames: readonly string[],
533
+ ): DeadCodeStats {
534
+ const unusedSet = new Set(unusedExportNames)
535
+
536
+ const byType: Record<string, {total: number; unused: number}> = {}
537
+
538
+ for (const exp of exports) {
539
+ const existing = byType[exp.type]
540
+ if (existing === undefined) {
541
+ byType[exp.type] = {
542
+ total: 1,
543
+ unused: unusedSet.has(exp.name) ? 1 : 0,
544
+ }
545
+ } else {
546
+ existing.total++
547
+ if (unusedSet.has(exp.name)) {
548
+ existing.unused++
549
+ }
550
+ }
551
+ }
552
+
553
+ const totalExports = exports.length
554
+ const unusedExports = unusedExportNames.length
555
+ const usedExports = totalExports - unusedExports
556
+ const usagePercentage = totalExports === 0 ? 100 : (usedExports / totalExports) * 100
557
+
558
+ return {
559
+ totalExports,
560
+ unusedExports,
561
+ usedExports,
562
+ usagePercentage,
563
+ byType,
564
+ }
565
+ }