(args: z.infer<typeof FindDeadCodeArgs>, ctx: ToolContext)
| 183 | argsSchema = FindDeadCodeArgs; |
| 184 | |
| 185 | async execute(args: z.infer<typeof FindDeadCodeArgs>, ctx: ToolContext): Promise<ToolResult> { |
| 186 | const root = args.path ? path.join(ctx.cwd, args.path) : ctx.cwd; |
| 187 | const maxFiles = args.max_files ?? 10_000; |
| 188 | const scope = args.scope ?? 'all'; |
| 189 | const includeTests = args.include_tests ?? false; |
| 190 | |
| 191 | const files = await walkSource(root, maxFiles); |
| 192 | |
| 193 | // package.json for entry-point hinting |
| 194 | let pkg: any = null; |
| 195 | let pkgUnparseable = false; |
| 196 | const pkgPath = path.join(ctx.cwd, 'package.json'); |
| 197 | try { |
| 198 | pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8')); |
| 199 | } catch (err: any) { |
| 200 | // A missing package.json is fine — just no entry-point hints. But a |
| 201 | // corrupt one means we proceed with NO deps/entry hints, which inflates |
| 202 | // false positives; warn so the user knows results may be unreliable. |
| 203 | if (err?.code !== 'ENOENT') { |
| 204 | pkgUnparseable = true; |
| 205 | logger.warn(`Failed to parse ${pkgPath}: ${err?.message ?? err}`); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | const lines: string[] = []; |
| 210 | lines.push(`# Dead Code Report`); |
| 211 | lines.push(`Scanned ${files.length} source file(s) in ${path.relative(ctx.cwd, root) || '.'}`); |
| 212 | lines.push(`Scope: ${scope}${includeTests ? ' (incl. tests)' : ' (excl. tests)'}`); |
| 213 | if (pkgUnparseable) { |
| 214 | lines.push(`⚠ package.json exists but could not be parsed — entry-point detection is degraded, so results below may contain extra false positives.`); |
| 215 | } |
| 216 | lines.push(''); |
| 217 | |
| 218 | // ─── 1. Orphaned files ─── |
| 219 | const orphanedFiles: { rel: string; isEntry: boolean; reason: string }[] = []; |
| 220 | if (scope === 'files' || scope === 'all') { |
| 221 | // For each non-test file, check if anyone references it |
| 222 | const refSources = includeTests ? files : files.filter(f => !f.isTest); |
| 223 | for (const target of files) { |
| 224 | if (target.isTest) continue; // we don't flag test files as dead |
| 225 | const isEntry = isLikelyEntryPoint(target.rel, pkg); |
| 226 | if (isEntry) continue; // skip entry points |
| 227 | |
| 228 | const patterns = buildReferencePatterns(target.rel); |
| 229 | let referenced = false; |
| 230 | for (const src of refSources) { |
| 231 | if (src.abs === target.abs) continue; |
| 232 | for (const pat of patterns) { |
| 233 | if (src.content.includes(pat)) { referenced = true; break; } |
| 234 | } |
| 235 | if (referenced) break; |
| 236 | } |
| 237 | if (!referenced) { |
| 238 | orphanedFiles.push({ |
| 239 | rel: target.rel, |
| 240 | isEntry: false, |
| 241 | reason: 'No imports detected. May still be referenced by config, dynamic import, or framework convention.', |
| 242 | }); |
nothing calls this directly
no test coverage detected