#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { extname, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
const DEFAULT_SOURCE_ROOT = 'src';
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
const IGNORED_DIRECTORIES = new Set(['.git', 'dist', 'node_modules']);
const MARKDOWN_TABLE_HEADER = '| Path | Export | Kind | Identifier refs |';
const MARKDOWN_TABLE_SEPARATOR = '| --- | --- | --- | ---: |';
export function buildDeadExportReport(sourceRoot = DEFAULT_SOURCE_ROOT) {
    const root = resolve(sourceRoot);
    const records = sourceRecords(root);
    const declarations = records.flatMap((record) => exportedDeclarations(record));
    const referenceCounts = identifierReferenceCounts(records);
    const findings = declarations
        .map((declaration) => findingForDeclaration(referenceCounts, declaration))
        .filter((finding) => finding.identifierReferences === 0)
        .sort(compareFindings);
    return Object.freeze({
        root,
        filesScanned: records.length,
        exportsScanned: declarations.length,
        findings,
    });
}
export function formatDeadExportReport(report) {
    const lines = [
        '# Dead Export Candidate Report',
        '',
        `Source root: ${report.root}`,
        `Files scanned: ${report.filesScanned}`,
        `Exports scanned: ${report.exportsScanned}`,
        `Candidates: ${report.findings.length}`,
        '',
        MARKDOWN_TABLE_HEADER,
        MARKDOWN_TABLE_SEPARATOR,
    ];
    for (const finding of report.findings) {
        lines.push(`| ${finding.path} | \`${finding.name}\` | ${finding.kind} | ${finding.identifierReferences} |`);
    }
    return `${lines.join('\n')}\n`;
}
function sourceRecords(root) {
    return sourceFilePaths(root).map((path) => {
        const text = readFileSync(path, 'utf8');
        return Object.freeze({
            relativePath: relative(root, path),
            sourceFile: ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path)),
        });
    });
}
function sourceFilePaths(root) {
    if (!existsSync(root)) {
        return [];
    }
    const paths = [];
    collectSourceFilePaths(root, paths);
    return paths.sort((left, right) => left.localeCompare(right));
}
function collectSourceFilePaths(directory, paths) {
    for (const entry of readdirSync(directory)) {
        if (IGNORED_DIRECTORIES.has(entry)) {
            continue;
        }
        const path = resolve(directory, entry);
        const stat = statSync(path);
        if (stat.isDirectory()) {
            collectSourceFilePaths(path, paths);
            continue;
        }
        if (stat.isFile() && SOURCE_EXTENSIONS.has(extname(path))) {
            paths.push(path);
        }
    }
}
function scriptKind(path) {
    const extension = extname(path);
    if (extension === '.js') {
        return ts.ScriptKind.JS;
    }
    if (extension === '.jsx') {
        return ts.ScriptKind.JSX;
    }
    if (extension === '.tsx') {
        return ts.ScriptKind.TSX;
    }
    return ts.ScriptKind.TS;
}
function exportedDeclarations(record) {
    const declarations = [];
    for (const statement of record.sourceFile.statements) {
        pushExportedStatement(record, statement, declarations);
    }
    return declarations;
}
function pushExportedStatement(record, statement, declarations) {
    if (ts.isExportDeclaration(statement)) {
        pushExportDeclaration(record, statement, declarations);
        return;
    }
    if (!hasExportModifier(statement)) {
        return;
    }
    pushDirectExportedStatement(record, statement, declarations);
}
function pushDirectExportedStatement(record, statement, declarations) {
    if (ts.isClassDeclaration(statement) && statement.name !== undefined) {
        pushDeclaration(record, declarations, statement.name.text, 'class');
        return;
    }
    if (ts.isFunctionDeclaration(statement) && statement.name !== undefined) {
        pushDeclaration(record, declarations, statement.name.text, 'function');
        return;
    }
    if (ts.isInterfaceDeclaration(statement)) {
        pushDeclaration(record, declarations, statement.name.text, 'interface');
        return;
    }
    if (ts.isTypeAliasDeclaration(statement)) {
        pushDeclaration(record, declarations, statement.name.text, 'type');
        return;
    }
    if (ts.isEnumDeclaration(statement)) {
        pushDeclaration(record, declarations, statement.name.text, 'enum');
        return;
    }
    if (ts.isVariableStatement(statement)) {
        pushVariableStatement(record, statement, declarations);
    }
}
function pushExportDeclaration(record, statement, declarations) {
    const exportClause = statement.exportClause;
    if (exportClause === undefined || !ts.isNamedExports(exportClause)) {
        return;
    }
    for (const element of exportClause.elements) {
        const referenceName = element.propertyName?.text ?? element.name.text;
        pushDeclaration(record, declarations, element.name.text, 're-export', referenceName);
    }
}
function pushVariableStatement(record, statement, declarations) {
    for (const declaration of statement.declarationList.declarations) {
        if (ts.isIdentifier(declaration.name)) {
            pushDeclaration(record, declarations, declaration.name.text, 'const');
        }
    }
}
function pushDeclaration(record, declarations, exportedName, kind, referenceName = exportedName) {
    declarations.push(Object.freeze({
        path: record.relativePath,
        exportedName,
        referenceName,
        kind,
    }));
}
function hasExportModifier(node) {
    if (!ts.canHaveModifiers(node)) {
        return false;
    }
    const modifiers = ts.getModifiers(node);
    if (modifiers === undefined) {
        return false;
    }
    return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
}
function findingForDeclaration(referenceCounts, declaration) {
    return Object.freeze({
        path: declaration.path,
        name: declaration.exportedName,
        kind: declaration.kind,
        identifierReferences: referenceCounts.get(declaration.referenceName) ?? 0,
    });
}
function identifierReferenceCounts(records) {
    const counts = new Map();
    for (const record of records) {
        collectIdentifierReferenceCounts(record.sourceFile, counts);
    }
    return counts;
}
function collectIdentifierReferenceCounts(node, counts) {
    if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
        counts.set(node.text, (counts.get(node.text) ?? 0) + 1);
    }
    ts.forEachChild(node, (child) => {
        collectIdentifierReferenceCounts(child, counts);
    });
}
function isReferenceIdentifier(identifier) {
    const parent = identifier.parent;
    if (parent === undefined) {
        return false;
    }
    return !isImportOrExportSpecifierName(identifier, parent) && !isDeclarationName(identifier, parent);
}
function isImportOrExportSpecifierName(identifier, parent) {
    if (!ts.isImportSpecifier(parent) && !ts.isExportSpecifier(parent)) {
        return false;
    }
    return parent.name === identifier || parent.propertyName === identifier;
}
function isDeclarationName(identifier, parent) {
    return (ts.isClassDeclaration(parent) && parent.name === identifier)
        || (ts.isFunctionDeclaration(parent) && parent.name === identifier)
        || (ts.isInterfaceDeclaration(parent) && parent.name === identifier)
        || (ts.isTypeAliasDeclaration(parent) && parent.name === identifier)
        || (ts.isEnumDeclaration(parent) && parent.name === identifier)
        || (ts.isVariableDeclaration(parent) && parent.name === identifier)
        || (ts.isParameter(parent) && parent.name === identifier);
}
function compareFindings(left, right) {
    const pathCompare = left.path.localeCompare(right.path);
    if (pathCompare !== 0) {
        return pathCompare;
    }
    return left.name.localeCompare(right.name);
}
function isDirectExecution() {
    const entry = process.argv[1];
    if (entry === undefined) {
        return false;
    }
    return resolve(entry) === fileURLToPath(import.meta.url);
}
if (isDirectExecution()) {
    process.stdout.write(formatDeadExportReport(buildDeadExportReport(process.argv[2] ?? DEFAULT_SOURCE_ROOT)));
}
