@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.
- package/README.md +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- 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
|
+
}
|