devghost
Version:
👻 Find dead code, dead imports, and dead dependencies before they haunt your project
295 lines • 12.1 kB
JavaScript
;
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