scripts/checkUnusedPods.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bun
/**
* Fails when a pod declared in ios/Podfile is no longer reached from the sources.
*
* Removing the last call site of a pod does not fail the build — the pod keeps
* being fetched, compiled and linked — so nothing tells us a declaration has
* outlived its reason. Knip cannot help here either: it reads JavaScript and
* TypeScript, and a Podfile is neither.
*
* Every declared pod needs an entry in ios/pod-usage.json saying how the sources
* reach it. `module` and `headers` entries are verified against the sources on
* every run. `exempt` entries are for pods that are alive by a route the sources
* cannot show — a build phase, a version pin, a linker flag — and they have to
* carry a reason and the date it was last checked. That date is validated as a
* real, non-future date and reported as a warning once it is over a year old; it
* is not a failure, so an exemption never breaks the build on a date nobody
* chose.
*
* The manifest is generated by scripts/generatePodUsage.ts after `pod install`.
*
* Usage: npx bun scripts/checkUnusedPods.ts
*/
import path from 'path';
import type {PodUsageEntry} from './podUsageShared';
import {findBlankEntry, validateReviewedDate} from './nativeUsageManifest';
import {MANIFEST_PATH, countReferences, findUnparsedPodDeclarations, readDeclaredPods, readManifest, readSources} from './podUsageShared';
const RELATIVE_MANIFEST = path.relative(path.resolve(__dirname, '..'), MANIFEST_PATH);
const USAGES = new Set(['module', 'headers', 'exempt']);
/**
* Modules every iOS source imports anyway, so claiming one proves nothing about
* a pod.
*/
const SYSTEM_MODULES = new Set(['Foundation', 'UIKit', 'SwiftUI', 'Combine', 'CoreGraphics', 'CoreFoundation', 'Swift', 'React', 'ObjectiveC']);
function validateEntry(pod: string, entry: PodUsageEntry): {error?: string; warning?: string} {
if (!USAGES.has(entry.usage)) {
return {error: `${pod}: "usage" must be one of ${[...USAGES].join(', ')}.`};
}
if (entry.usage === 'module') {
if (!entry.module.trim()) {
return {error: `${pod}: a "module" entry needs the module name the pod is imported under.`};
}
if (SYSTEM_MODULES.has(entry.module.trim())) {
return {
error: `${pod}: "${entry.module}" is a system module that the sources import regardless of this pod, so it cannot verify it. Use the pod's own module, or mark it "exempt" with a reason.`,
};
}
}
if (entry.usage === 'headers') {
if (!entry.headers.length) {
return {error: `${pod}: a "headers" entry needs at least one public header name.`};
}
const blank = findBlankEntry(entry.headers, pod, 'headers');
if (blank) {
return {error: blank};
}
}
if (entry.usage === 'exempt') {
if (!entry.reason.trim()) {
return {error: `${pod}: an "exempt" entry needs a "reason" saying what keeps the pod alive.`};
}
return validateReviewedDate(entry.reviewed, pod);
}
return {};
}
/**
* Two pods claiming the same module or header cannot be told apart: whichever
* one still has a call site keeps the other green.
*/
function findOverlappingRoutes(manifest: Record<string, PodUsageEntry>): string[] {
const claims = Object.entries(manifest).flatMap(([pod, entry]) => {
if (entry.usage === 'module') {
return [{pod, route: `module ${entry.module}`}];
}
if (entry.usage === 'headers') {
return entry.headers.map((header) => ({pod, route: `header ${header}`}));
}
return [];
});
const problems: string[] = [];
for (const [index, claim] of claims.entries()) {
for (const other of claims.slice(index + 1)) {
if (other.pod !== claim.pod && other.route === claim.route) {
problems.push(`${claim.pod} and ${other.pod} both claim ${claim.route}, so a reference cannot tell them apart. Mark the one that does not own it "exempt" with a reason.`);
}
}
}
return problems;
}
function main() {
let manifest;
try {
manifest = readManifest();
} catch (error) {
console.error(`Unused pod check failed:\n\n - ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
const declared = readDeclaredPods();
const problems: string[] = [];
const warnings: string[] = [];
problems.push(...findOverlappingRoutes(manifest));
for (const unparsed of findUnparsedPodDeclarations()) {
problems.push(`${unparsed}\n This pod declaration is in a form the check cannot read, so it would be verified by nothing. Teach scripts/podUsageShared.ts to read it.`);
}
// A parser that matches nothing would otherwise report success over an empty
// set, which is the one failure mode this check cannot afford.
if (!declared.length) {
problems.push('No pods were parsed out of ios/Podfile. Either the Podfile declares none, or the parser in scripts/podUsageShared.ts no longer understands it.');
}
for (const pod of declared) {
if (!(pod in manifest)) {
problems.push(`${pod} is declared in ios/Podfile but missing from ${RELATIVE_MANIFEST}. Run \`npx bun scripts/generatePodUsage.ts\` after \`pod install\`.`);
}
}
for (const pod of Object.keys(manifest)) {
if (!declared.includes(pod)) {
problems.push(`${pod} is in ${RELATIVE_MANIFEST} but no longer declared in ios/Podfile. Delete the entry.`);
}
}
const sources = readSources();
for (const pod of declared) {
const entry = manifest[pod];
if (!entry) {
continue;
}
const {error, warning} = validateEntry(pod, entry);
if (warning) {
warnings.push(warning);
}
if (error) {
problems.push(error);
continue;
}
if (entry.usage === 'exempt') {
continue;
}
if (countReferences(sources, entry, pod) === 0) {
const route = entry.usage === 'module' ? `import ${entry.module}` : entry.headers.map((header) => `#import "${header}"`).join(' / ');
problems.push(`${pod} is declared in ios/Podfile but nothing in ios/ reaches it (looked for ${route}). Delete the pod, or mark it "exempt" with a reason.`);
}
}
if (warnings.length) {
console.warn('Unused pod check warnings:\n');
for (const warning of warnings) {
console.warn(` - ${warning}`);
}
console.warn('');
}
if (problems.length) {
console.error('Unused pod check failed:\n');
for (const problem of problems) {
console.error(` - ${problem}`);
}
process.exit(1);
}
console.log(`Unused pod check passed: ${declared.length} pods declared, all reached or exempt with a reason.`);
}
main();