Ctrl K
ComunidadGuías…

scripts/checkUnusedGradleDependencies.ts

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bun
/**
 * Fails when a dependency declared in android/app/build.gradle is no longer
 * reached from the project.
 *
 * Android has no committed lockfile and no build failure to warn us: a Gradle
 * declaration whose last consumer was deleted keeps being resolved, compiled and
 * packaged. Knip cannot help either — it reads JavaScript and TypeScript. This
 * check is what notices.
 *
 * Every declared coordinate needs an entry in android/app/dependency-usage.json.
 * `packages` entries are verified against Java and Kotlin imports, `resources`
 * entries against the XML — a view class in a layout, a style parent, an attr.
 * `exempt` is for the artifacts that are genuinely needed with nothing in the
 * project to prove it — a version pin, a BOM, a library React Native reaches
 * rather than our own code — and each one carries 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.
 *
 * Usage: npx bun scripts/checkUnusedGradleDependencies.ts
 */

import path from 'path';

import type {GradleUsageEntry} from './gradleUsageShared';

import {MANIFEST_PATH, RELATIVE_BUILD_GRADLE, countReferences, findUnparsedDeclarations, readDeclaredDependencies, readManifest, readResources, readSources} from './gradleUsageShared';
import {findBlankEntry, validateReviewedDate} from './nativeUsageManifest';

const RELATIVE_MANIFEST = path.relative(path.resolve(__dirname, '..'), MANIFEST_PATH);
const USAGES = new Set(['packages', 'resources', 'exempt']);
const MINIMUM_RESOURCE_NAME_LENGTH = 8;

function validateEntry(coordinate: string, entry: GradleUsageEntry): {error?: string; warning?: string} {
    if (!USAGES.has(entry.usage)) {
        return {error: `${coordinate}: "usage" must be one of ${[...USAGES].join(', ')}.`};
    }
    if (entry.usage === 'packages') {
        if (!entry.packages.length) {
            return {error: `${coordinate}: a "packages" entry needs at least one Java package prefix.`};
        }
        const blank = findBlankEntry(entry.packages, coordinate, 'packages');
        if (blank) {
            return {error: blank};
        }
    }
    if (entry.usage === 'resources') {
        if (!entry.resources.length) {
            return {error: `${coordinate}: a "resources" entry needs at least one name to look for in the XML.`};
        }
        const blank = findBlankEntry(entry.resources, coordinate, 'resources');
        if (blank) {
            return {error: blank};
        }
        // An unqualified or short name matches too much of the XML to prove
        // anything: `name` alone occurs in hundreds of places, so a dead
        // coordinate could be made permanently green with it.
        const vague = entry.resources.find((name) => name.trim().length < MINIMUM_RESOURCE_NAME_LENGTH || !name.includes('.'));
        if (vague) {
            return {
                error: `${coordinate}: the "resources" name ${vague} is not specific enough to identify the dependency. Use the fully qualified class or attr name, at least ${MINIMUM_RESOURCE_NAME_LENGTH} characters and containing a dot.`,
            };
        }
    }
    if (entry.usage === 'exempt') {
        if (!entry.reason.trim()) {
            return {error: `${coordinate}: an "exempt" entry needs a "reason" saying what keeps the dependency alive.`};
        }
        return validateReviewedDate(entry.reviewed, coordinate);
    }
    return {};
}

/**
 * Two coordinates whose package prefixes overlap cannot be told apart: whichever
 * one still has a consumer keeps the other green. That is how a group-wide prefix
 * such as `com.facebook.react` would "verify" `hermes-android` off imports that
 * belong to `react-android`. A hand-written prefix this wide is legitimate only
 * while it is the sole claim on that namespace.
 */
function findOverlappingPackages(manifest: Record<string, GradleUsageEntry>): string[] {
    const claims = Object.entries(manifest).flatMap(([coordinate, entry]) => (entry.usage === 'packages' ? entry.packages.map((prefix) => ({coordinate, prefix})) : []));
    const problems: string[] = [];
    for (const [index, claim] of claims.entries()) {
        for (const other of claims.slice(index + 1)) {
            if (other.coordinate === claim.coordinate) {
                continue;
            }
            const overlaps = claim.prefix === other.prefix || claim.prefix.startsWith(`${other.prefix}.`) || other.prefix.startsWith(`${claim.prefix}.`);
            if (overlaps) {
                problems.push(
                    `${claim.coordinate} and ${other.coordinate} both claim overlapping packages (${claim.prefix} and ${other.prefix}), so an import cannot tell them apart. Narrow one of them, or mark the artifact that does not own the package "exempt" with a reason.`,
                );
            }
        }
    }
    return problems;
}

function main() {
    let manifest;
    try {
        manifest = readManifest();
    } catch (error) {
        console.error(`Unused Gradle dependency check failed:\n\n  - ${error instanceof Error ? error.message : String(error)}`);
        process.exit(1);
    }
    const declared = readDeclaredDependencies();
    const problems: string[] = [];
    const warnings: string[] = [];

    problems.push(...findOverlappingPackages(manifest));

    for (const unparsed of findUnparsedDeclarations()) {
        problems.push(`${unparsed}\n    This declaration is in a form the check cannot read, so it would be verified by nothing. Teach scripts/gradleUsageShared.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 dependencies were parsed out of ${RELATIVE_BUILD_GRADLE}. Either it declares none, or the parser in scripts/gradleUsageShared.ts no longer understands it.`);
    }

    for (const coordinate of declared) {
        if (!(coordinate in manifest)) {
            problems.push(`${coordinate} is declared in android/app/build.gradle but missing from ${RELATIVE_MANIFEST}. Run \`npx bun scripts/generateGradleDependencyUsage.ts\`.`);
        }
    }

    for (const coordinate of Object.keys(manifest)) {
        if (!declared.includes(coordinate)) {
            problems.push(`${coordinate} is in ${RELATIVE_MANIFEST} but no longer declared in android/app/build.gradle. Delete the entry.`);
        }
    }

    const context = {sources: readSources(), resources: readResources()};
    for (const coordinate of declared) {
        const entry = manifest[coordinate];
        if (!entry) {
            continue;
        }
        const {error, warning} = validateEntry(coordinate, entry);
        if (warning) {
            warnings.push(warning);
        }
        if (error) {
            problems.push(error);
            continue;
        }
        if (entry.usage === 'exempt') {
            continue;
        }
        if (countReferences(context, entry) === 0) {
            const route = entry.usage === 'packages' ? `imports of ${entry.packages.join(', ')}` : `${entry.resources.join(', ')} in the XML`;
            problems.push(`${coordinate} is declared in android/app/build.gradle but nothing reaches it (looked for ${route}). Delete the dependency, or mark it "exempt" with a reason.`);
        }
    }

    if (warnings.length) {
        console.warn('Unused Gradle dependency check warnings:\n');
        for (const warning of warnings) {
            console.warn(`  - ${warning}`);
        }
        console.warn('');
    }

    if (problems.length) {
        console.error('Unused Gradle dependency check failed:\n');
        for (const problem of problems) {
            console.error(`  - ${problem}`);
        }
        process.exit(1);
    }

    console.log(`Unused Gradle dependency check passed: ${declared.length} dependencies declared, all reached or exempt with a reason.`);
}

main();