Ctrl K
ComunidadGuías…

scripts/nativeSourceComments.ts

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

Archivo original ↗Preguntar a KiBot
/**
 * Comment stripping for the native dependency checks.
 *
 * Both checks decide whether a declaration is alive by matching patterns against
 * source text. Matching raw text counts a commented-out import as a live
 * reference, and reads a commented-out declaration as a real one, so every
 * declaration a developer parks behind a comment while debugging either keeps a
 * dead dependency green or fails the check for something Gradle never sees.
 * Stripping comments first is what makes both answers honest.
 *
 * Newlines inside a stripped comment are preserved so that line-anchored
 * patterns keep matching the lines they were written for, and string bodies are
 * emitted verbatim so that this can never delete code.
 */

/**
 * Swift and Kotlin nest block comments; C, C++, Objective-C, Java and Groovy do
 * not. Treating a nested opener as nesting in the languages that do not would
 * swallow every line between the comment's real end and the next closer, which
 * silently shrinks the set of declarations and imports the checks can see.
 */
type CommentFlavor = 'c' | 'swift' | 'kotlin' | 'ruby' | 'xml';

const NESTING_FLAVORS = new Set<CommentFlavor>(['swift', 'kotlin']);

/**
 * Swift has no char literal, so a lone apostrophe there is punctuation rather
 * than the start of one. Everywhere else `'...'` is a char literal, or in Groovy
 * a string, and has to be consumed as one.
 */
const CHAR_LITERAL_FLAVORS = new Set<CommentFlavor>(['c', 'kotlin']);

/**
 * The comment flavor a file extension implies. Anything unrecognized is treated
 * as the non-nesting C family, which is the safe default: it can leave a comment
 * standing, but it can never discard code.
 */
function flavorForExtension(extension: string): CommentFlavor {
    if (extension === '.swift') {
        return 'swift';
    }
    if (extension === '.kt' || extension === '.kts') {
        return 'kotlin';
    }
    if (extension === '.xml') {
        return 'xml';
    }
    return 'c';
}

/**
 * The delimiter that closes a string starting at `index`, and its length, or
 * undefined when no string starts there. Handles `"`, triple-quoted blocks, and
 * Swift raw strings such as `#"..."#` and `##"..."##`.
 */
function stringOpener(source: string, index: number, flavor: CommentFlavor): {open: string; close: string} | undefined {
    if (flavor === 'swift') {
        const raw = /^(#+)"(#*)/.exec(source.slice(index, index + 16));
        const hashes = raw?.at(1);
        if (hashes) {
            const isTriple = source.startsWith(`${hashes}"""`, index);
            return {open: isTriple ? `${hashes}"""` : `${hashes}"`, close: isTriple ? `"""${hashes}` : `"${hashes}`};
        }
    }
    if (source.startsWith('"""', index)) {
        return {open: '"""', close: '"""'};
    }
    if (source[index] === '"') {
        return {open: '"', close: '"'};
    }
    if (source[index] === "'" && CHAR_LITERAL_FLAVORS.has(flavor)) {
        return {open: "'", close: "'"};
    }
    return undefined;
}

function stripCFamilyComments(source: string, flavor: CommentFlavor): string {
    const nested = NESTING_FLAVORS.has(flavor);
    let output = '';
    let index = 0;
    let blockDepth = 0;

    while (index < source.length) {
        if (blockDepth > 0) {
            if (nested && source.startsWith('/*', index)) {
                blockDepth++;
                index += 2;
            } else if (source.startsWith('*/', index)) {
                blockDepth--;
                index += 2;
            } else {
                if (source[index] === '\n') {
                    output += '\n';
                }
                index++;
            }
            continue;
        }

        if (source.startsWith('//', index)) {
            while (index < source.length && source[index] !== '\n') {
                index++;
            }
            continue;
        }

        if (source.startsWith('/*', index)) {
            blockDepth = 1;
            index += 2;
            continue;
        }

        const delimiter = stringOpener(source, index, flavor);
        if (delimiter) {
            output += delimiter.open;
            index += delimiter.open.length;
            while (index < source.length) {
                if (source[index] === '\\') {
                    output += source.slice(index, index + 2);
                    index += 2;
                    continue;
                }
                if (source.startsWith(delimiter.close, index)) {
                    // A triple-quoted body ending in a quote puts four or more
                    // quotes in a row; the closer is the last three of the run.
                    let run = index;
                    while (delimiter.close.startsWith('"""') && source[run + delimiter.close.length] === '"') {
                        output += source[run];
                        run++;
                    }
                    output += source.slice(run, run + delimiter.close.length);
                    index = run + delimiter.close.length;
                    break;
                }
                // An unterminated char literal is far more likely to be an
                // apostrophe than a literal, so do not let it swallow the file.
                if (source[index] === '\n' && delimiter.close === "'") {
                    break;
                }
                output += source[index];
                index++;
            }
            continue;
        }

        output += source[index];
        index++;
    }

    return output;
}

/**
 * `#` line comments and `=begin` / `=end` blocks, leaving `#` inside strings and
 * `#{}` interpolation alone.
 */
function stripRubyComments(source: string): string {
    const lines = source.split('\n');
    const output: string[] = [];
    let inBlock = false;

    for (const line of lines) {
        if (inBlock) {
            output.push('');
            if (/^=end\b/.test(line)) {
                inBlock = false;
            }
            continue;
        }
        if (/^=begin\b/.test(line)) {
            inBlock = true;
            output.push('');
            continue;
        }

        let stripped = '';
        let quote: string | undefined;
        for (let index = 0; index < line.length; index++) {
            const character = line[index];
            if (quote) {
                stripped += character;
                if (character === '\\') {
                    stripped += line[index + 1] ?? '';
                    index++;
                } else if (character === quote) {
                    quote = undefined;
                }
                continue;
            }
            if (character === '"' || character === "'") {
                quote = character;
                stripped += character;
                continue;
            }
            if (character === '#') {
                break;
            }
            stripped += character;
        }
        output.push(stripped);
    }

    return output.join('\n');
}

function stripXmlComments(source: string): string {
    // An unterminated opener runs to the end of the file; leaving it in place
    // would keep whatever was commented out at the end of a layout counting as a
    // live resource reference.
    return source.replaceAll(/<!--(?:[\s\S]*?-->|[\s\S]*$)/g, (comment) => comment.replaceAll(/[^\n]/g, ''));
}

function stripComments(source: string, flavor: CommentFlavor): string {
    if (flavor === 'ruby') {
        return stripRubyComments(source);
    }
    if (flavor === 'xml') {
        return stripXmlComments(source);
    }
    return stripCFamilyComments(source, flavor);
}

export type {CommentFlavor};
export {stripComments, flavorForExtension};