UNPKG

devghost

Version:

👻 Find dead code, dead imports, and dead dependencies before they haunt your project

295 lines • 12.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeUnusedExports = analyzeUnusedExports; const fs = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const ts = __importStar(require("typescript")); const fs_1 = require("../utils/fs"); const tsparser_1 = require("../utils/tsparser"); /** * Resolve relative import path to absolute file path */ function resolveImportPath(importerFile, importPath) { if (!importPath.startsWith('.')) { return null; // Not a relative import } const importerDir = path.dirname(importerFile); let resolvedPath = path.resolve(importerDir, importPath); // Normalize to consistent path separators resolvedPath = path.normalize(resolvedPath); // Try different extensions const extensions = ['.ts', '.tsx', '.js', '.jsx']; // First, try with direct extensions for (const ext of extensions) { const testPath = resolvedPath + ext; if (fs.existsSync(testPath)) { return path.normalize(testPath); } } // If the path already has an extension and exists AS A FILE (not directory) if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) { return path.normalize(resolvedPath); } // Try as a directory with index file for (const ext of extensions) { const testPath = path.join(resolvedPath, `index${ext}`); if (fs.existsSync(testPath)) { return path.normalize(testPath); } } return null; } /** * Extract all exports from a source file */ function extractExportsFromFile(filePath) { const exports = []; const sourceFile = (0, tsparser_1.parseFile)(filePath); if (!sourceFile) { return exports; } const fileContent = fs.readFileSync(filePath, 'utf-8'); function visit(node) { // Handle: export function foo() {} // Handle: export const bar = ... // Handle: export class Baz {} if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isVariableStatement(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) { const hasDefault = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword); const exportType = hasDefault ? 'default' : 'named'; let exportName = ''; if (ts.isVariableStatement(node)) { // export const foo = ..., bar = ... node.declarationList.declarations.forEach((decl) => { if (ts.isIdentifier(decl.name)) { if (!sourceFile) return; const { line, column } = (0, tsparser_1.getLineAndColumn)(sourceFile, decl.name.getStart()); if (!(0, fs_1.hasIgnoreComment)(fileContent, line)) { exports.push({ file: filePath, name: decl.name.text, line, column, exportType, entireLine: (0, tsparser_1.getLineText)(sourceFile, line), }); } } }); return; // Already processed } else if (node.name && ts.isIdentifier(node.name)) { exportName = node.name.text; } else if (hasDefault) { exportName = 'default'; } if (exportName) { if (!sourceFile) return; const { line, column } = (0, tsparser_1.getLineAndColumn)(sourceFile, node.getStart()); if (!(0, fs_1.hasIgnoreComment)(fileContent, line)) { exports.push({ file: filePath, name: exportName, line, column, exportType, entireLine: (0, tsparser_1.getLineText)(sourceFile, line), }); } } } // Handle: export { x, y, z } // Handle: export { x as y } // Handle: export { x as y } from './other' (re-exports) if (ts.isExportDeclaration(node)) { if (node.exportClause && ts.isNamedExports(node.exportClause)) { node.exportClause.elements.forEach((element) => { // For re-exports or direct exports, we care about the exported name (after 'as') const exportName = element.name.text; // This is the name being exported if (!sourceFile) return; const { line, column } = (0, tsparser_1.getLineAndColumn)(sourceFile, element.getStart()); if (!(0, fs_1.hasIgnoreComment)(fileContent, line)) { exports.push({ file: filePath, name: exportName, line, column, exportType: 'named', entireLine: (0, tsparser_1.getLineText)(sourceFile, line), }); } }); } } // Handle: export default ... if (ts.isExportAssignment(node)) { if (!sourceFile) return; const { line, column } = (0, tsparser_1.getLineAndColumn)(sourceFile, node.getStart()); if (!(0, fs_1.hasIgnoreComment)(fileContent, line)) { exports.push({ file: filePath, name: 'default', line, column, exportType: 'default', entireLine: (0, tsparser_1.getLineText)(sourceFile, line), }); } } ts.forEachChild(node, visit); } visit(sourceFile); return exports; } /** * Extract all imports from a source file and resolve their target files */ function extractImportsFromFile(filePath) { // Map: resolved file path -> set of imported names const imports = new Map(); const sourceFile = (0, tsparser_1.parseFile)(filePath); if (!sourceFile) { return imports; } function visit(node) { if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { const importPath = node.moduleSpecifier.text; // Resolve the import path to actual file const resolvedPath = resolveImportPath(filePath, importPath); if (!resolvedPath) { return; // Skip external packages } if (!imports.has(resolvedPath)) { imports.set(resolvedPath, new Set()); } const importedNames = imports.get(resolvedPath); if (!importedNames) return; if (node.importClause) { // Default import: import Foo from './bar' if (node.importClause.name && importedNames) { importedNames.add('default'); } // Named imports: import { x, y } from './bar' if (node.importClause.namedBindings) { if (ts.isNamedImports(node.importClause.namedBindings)) { node.importClause.namedBindings.elements.forEach((element) => { // Use the original name being imported (before 'as') const importedName = element.propertyName?.text || element.name.text; if (importedNames) importedNames.add(importedName); }); } else if (ts.isNamespaceImport(node.importClause.namedBindings)) { // import * as foo from './bar' - imports EVERYTHING if (importedNames) importedNames.add('*'); } } } } ts.forEachChild(node, visit); } visit(sourceFile); return imports; } /** * Analyze all files for unused exports */ async function analyzeUnusedExports(files) { // Step 1: Extract all exports from all files const allExports = []; for (const file of files) { const fileExports = extractExportsFromFile(file); allExports.push(...fileExports); } // Step 2: Build a comprehensive map of all imports // Map: target file path -> set of imported names const importMap = new Map(); for (const file of files) { const fileImports = extractImportsFromFile(file); fileImports.forEach((importedNames, targetFile) => { if (!importMap.has(targetFile)) { importMap.set(targetFile, new Set()); } // Merge imported names importedNames.forEach((name) => { importMap.get(targetFile)?.add(name); }); }); } // Step 3: Find unused exports by cross-referencing const unusedExports = []; for (const exportInfo of allExports) { const importedNames = importMap.get(exportInfo.file); // If no imports from this file at all, or this specific export isn't imported if (!importedNames) { // No one imports from this file at all unusedExports.push({ file: exportInfo.file, line: exportInfo.line, column: exportInfo.column, exportName: exportInfo.name, exportType: exportInfo.exportType, entireLine: exportInfo.entireLine, }); } else if (!importedNames.has('*') && !importedNames.has(exportInfo.name)) { // Someone imports from this file, but not this specific export // (and it's not a wildcard import) unusedExports.push({ file: exportInfo.file, line: exportInfo.line, column: exportInfo.column, exportName: exportInfo.name, exportType: exportInfo.exportType, entireLine: exportInfo.entireLine, }); } } return unusedExports; } //# sourceMappingURL=unusedExports.js.map