Ctrl K
ComunidadGuías…

scripts/checkUnusedPods.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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();