This commit is contained in:
parent
39b0160064
commit
2347ecd201
71 changed files with 3799 additions and 1606 deletions
185
scripts/check-unused-exports.mjs
Normal file
185
scripts/check-unused-exports.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
const sourceRoot = path.join(projectRoot, 'src');
|
||||
|
||||
const toPosix = (value) => value.split(path.sep).join('/');
|
||||
|
||||
const listTypeScriptFiles = (directory) =>
|
||||
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const entryPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return listTypeScriptFiles(entryPath);
|
||||
}
|
||||
return entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')
|
||||
? [entryPath]
|
||||
: [];
|
||||
});
|
||||
|
||||
const files = listTypeScriptFiles(sourceRoot);
|
||||
const fileSet = new Set(files.map((file) => path.resolve(file)));
|
||||
|
||||
const resolveModule = (fromFile, specifier) => {
|
||||
if (!specifier.startsWith('.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base = path.resolve(path.dirname(fromFile), specifier);
|
||||
const candidates = [
|
||||
`${base}.ts`,
|
||||
path.join(base, 'index.ts'),
|
||||
base.endsWith('.ts') ? base : null,
|
||||
].filter(Boolean);
|
||||
|
||||
return candidates.find((candidate) => existsSync(candidate) && fileSet.has(candidate)) ?? null;
|
||||
};
|
||||
|
||||
const exportKey = (file, name) => `${path.resolve(file)}:${name}`;
|
||||
const isExported = (node) =>
|
||||
ts.canHaveModifiers(node) &&
|
||||
(ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
||||
const isDefaultExported = (node) =>
|
||||
ts.canHaveModifiers(node) &&
|
||||
(ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
||||
|
||||
const exportedDeclarations = new Map();
|
||||
const usedExports = new Set();
|
||||
const wildcardUsedFiles = new Set();
|
||||
|
||||
const markUsed = (fromFile, name) => {
|
||||
usedExports.add(exportKey(fromFile, name));
|
||||
};
|
||||
|
||||
const collectImportUsage = (file, sourceFile) => {
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
const resolved = resolveModule(file, node.moduleSpecifier.text);
|
||||
if (!resolved || !node.importClause) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.importClause.name) {
|
||||
markUsed(resolved, 'default');
|
||||
}
|
||||
|
||||
const namedBindings = node.importClause.namedBindings;
|
||||
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
||||
namedBindings.elements.forEach((element) => {
|
||||
markUsed(resolved, (element.propertyName ?? element.name).text);
|
||||
});
|
||||
} else if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
||||
wildcardUsedFiles.add(path.resolve(resolved));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
node.moduleSpecifier &&
|
||||
ts.isStringLiteral(node.moduleSpecifier)
|
||||
) {
|
||||
const resolved = resolveModule(file, node.moduleSpecifier.text);
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.exportClause) {
|
||||
wildcardUsedFiles.add(path.resolve(resolved));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isNamedExports(node.exportClause)) {
|
||||
node.exportClause.elements.forEach((element) => {
|
||||
markUsed(resolved, (element.propertyName ?? element.name).text);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const collectExportedDeclarations = (file, sourceFile) => {
|
||||
if (file.endsWith('.test.ts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (ts.isVariableStatement(node) && isExported(node)) {
|
||||
node.declarationList.declarations.forEach((declaration) => {
|
||||
if (ts.isIdentifier(declaration.name)) {
|
||||
exportedDeclarations.set(exportKey(file, declaration.name.text), {
|
||||
file,
|
||||
name: declaration.name.text,
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(ts.isFunctionDeclaration(node) ||
|
||||
ts.isClassDeclaration(node) ||
|
||||
ts.isInterfaceDeclaration(node) ||
|
||||
ts.isTypeAliasDeclaration(node) ||
|
||||
ts.isEnumDeclaration(node)) &&
|
||||
isExported(node)
|
||||
) {
|
||||
if (isDefaultExported(node)) {
|
||||
exportedDeclarations.set(exportKey(file, 'default'), { file, name: 'default' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.name) {
|
||||
exportedDeclarations.set(exportKey(file, node.name.text), {
|
||||
file,
|
||||
name: node.name.text,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isExportDeclaration(node) &&
|
||||
!node.moduleSpecifier &&
|
||||
node.exportClause &&
|
||||
ts.isNamedExports(node.exportClause)
|
||||
) {
|
||||
node.exportClause.elements.forEach((element) => {
|
||||
exportedDeclarations.set(exportKey(file, element.name.text), {
|
||||
file,
|
||||
name: element.name.text,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parsedFiles = files.map((file) => ({
|
||||
file,
|
||||
sourceFile: ts.createSourceFile(
|
||||
file,
|
||||
readFileSync(file, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
),
|
||||
}));
|
||||
|
||||
parsedFiles.forEach(({ file, sourceFile }) => collectImportUsage(file, sourceFile));
|
||||
parsedFiles.forEach(({ file, sourceFile }) => collectExportedDeclarations(file, sourceFile));
|
||||
|
||||
const unusedExports = Array.from(exportedDeclarations.entries())
|
||||
.filter(([key, declaration]) => !usedExports.has(key) && !wildcardUsedFiles.has(declaration.file))
|
||||
.map(([, declaration]) => declaration)
|
||||
.sort((left, right) =>
|
||||
`${left.file}:${left.name}`.localeCompare(`${right.file}:${right.name}`)
|
||||
);
|
||||
|
||||
if (unusedExports.length > 0) {
|
||||
console.error('Unused exported declarations found:');
|
||||
unusedExports.forEach(({ file, name }) => {
|
||||
console.error(`- ${toPosix(path.relative(projectRoot, file))}: ${name}`);
|
||||
});
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue