"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearWalkCache = clearWalkCache;
exports.walkSourceFiles = walkSourceFiles;
exports.countDirectories = countDirectories;
exports.countLineMatches = countLineMatches;
exports.commentSyntaxFor = commentSyntaxFor;
exports.isCommentLine = isCommentLine;
exports.packSourceExtensions = packSourceExtensions;
/**
 * G_v4_7 (2.4.7) — canonical source-file walker + line-match counter.
 *
 * Every consumer that needs "the list of source files in this repo"
 * routes through `walkSourceFiles`. Every consumer that needs "how
 * many lines match pattern P across source files" routes through
 * `countLineMatches`. ONE walker, ONE counter — replaces the four
 * site-specific implementations that were drifting (D075, D079).
 *
 * NEVER shells out. Pre-fix `grep -rEf <pat> --include=*.js .` on
 * a JS-heavy customer frontend produced 67MB of stdout (matched minified .js content)
 * and silently failed with ENOBUFS — `run()`'s 64MB ceiling truncated
 * to empty string, `consoleLogCount` reported 0 when reality was ~1370
 * (D082/D083). Walking the curated set in JS and scanning each file's
 * lines in-process eliminates the maxBuffer ceiling entirely.
 *
 * Filter pipeline (per file encountered):
 *   1. Directory prune at entry: `isExcludedPath(cwd, relDir)` → never descend.
 *   2. Extension filter (default `allSourceExtensions()`).
 *   3. Path-pattern filter: `isExcludedPath(cwd, relPath)`.
 *   4. Autogen basename glob (`*.designer.cs`, `*.pb.go`, …).
 *   5. Autogen header marker (first 2KB).
 *   6. Test-file filter (skip unless `includeTests: true`).
 *   7. Pack scoping (when `opts.packId` set).
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const exclusions_1 = require("./exclusions");
const autogen_header_1 = require("./autogen-header");
const minified_detection_1 = require("./minified-detection");
const languages_1 = require("../../languages");
/** Cache: one entry per (cwd, opts-fingerprint). Cleared via
 *  `clearWalkCache()`. Same memo pattern as `loadExclusions`. */
const walkCache = new Map();
function clearWalkCache() {
    walkCache.clear();
}
/**
 * Walk the source tree rooted at `cwd` and return relative POSIX paths
 * (no leading `./`) sorted for determinism. Returns `[]` for non-
 * existent or non-directory roots — never throws.
 */
function walkSourceFiles(cwd, opts = {}) {
    const resolved = resolveOpts(opts);
    const cacheKey = `${cwd}\0${cacheKeyFor(opts)}`;
    const hit = walkCache.get(cacheKey);
    if (hit)
        return hit;
    const out = [];
    try {
        walkDir(cwd, '', resolved, out);
    }
    catch {
        // I/O error mid-walk: surface what we found so far.
    }
    out.sort();
    walkCache.set(cacheKey, out);
    return out;
}
function resolveOpts(opts) {
    // Precedence: explicit `extensions` > pack-scoped extensions > all packs.
    // Registry calls are made ONLY when the caller leaves a default to
    // fill — see the module-cycle note above for why the pack path must
    // never touch the registry.
    let exts;
    if (opts.extensions) {
        exts = opts.extensions;
    }
    else if (opts.packId) {
        exts = (0, languages_1.getLanguage)(opts.packId)?.sourceExtensions ?? [];
    }
    else {
        exts = (0, languages_1.allSourceExtensions)();
    }
    const includeTests = opts.includeTests ?? false;
    const includeAutogen = opts.includeAutogen ?? false;
    return {
        extensions: new Set(exts.map((e) => (e.startsWith('.') ? e : `.${e}`))),
        includeTests,
        includeAutogen,
        respectIgnore: opts.respectIgnore ?? true,
        // Only resolve autogen / test patterns from the registry when they
        // will actually be used as a filter (i.e. when NOT including them).
        autogenBasenamePatterns: includeAutogen ? [] : (0, languages_1.allAutogenSourcePatterns)(),
        testFilePatterns: includeTests
            ? { nameOnly: [], pathAnchored: [] }
            : splitTestPatterns((0, languages_1.allTestFilePatterns)()),
    };
}
function cacheKeyFor(opts) {
    return JSON.stringify([
        opts.extensions ?? null,
        opts.includeTests ?? false,
        opts.includeAutogen ?? false,
        opts.respectIgnore ?? true,
        opts.packId ?? null,
    ]);
}
function walkDir(cwd, relDir, opts, out) {
    const absDir = relDir ? path.join(cwd, relDir) : cwd;
    let entries;
    try {
        entries = fs.readdirSync(absDir, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const ent of entries) {
        const relPath = relDir ? `${relDir}/${ent.name}` : ent.name;
        if (ent.isDirectory()) {
            // Prune at directory boundary so node_modules/ etc. are never
            // descended into. Massive perf win on minified-JS-heavy repos.
            if (opts.respectIgnore && (0, exclusions_1.isExcludedPath)(cwd, relPath))
                continue;
            // Skip dot-directories ALWAYS (.git, .vscode, .idea, .dxkit…).
            // These rarely appear in `.gitignore` but are universally noise.
            if (ent.name.startsWith('.'))
                continue;
            walkDir(cwd, relPath, opts, out);
            continue;
        }
        if (!ent.isFile())
            continue;
        // Extension gate first (cheap).
        const ext = path.extname(ent.name);
        if (!opts.extensions.has(ext))
            continue;
        // Pack scoping (cheap — done after extension because packId is rare).
        if (opts.respectIgnore && (0, exclusions_1.isExcludedPath)(cwd, relPath))
            continue;
        // Test-file filter.
        if (!opts.includeTests && isTestFile(relPath, ent.name, opts.testFilePatterns))
            continue;
        // Autogen basename glob.
        if (!opts.includeAutogen && matchesAnyBasenameGlob(ent.name, opts.autogenBasenamePatterns)) {
            continue;
        }
        // Autogen header marker (most expensive — reads first 2KB).
        if (!opts.includeAutogen && (0, autogen_header_1.isAutogeneratedByHeader)(path.join(absDir, ent.name)))
            continue;
        // Minified / bundled output detection — catches webpack/vite/esbuild
        // hash-suffixed chunks, CDN-downloaded vendor libs dropped into
        // public/, and pre-minified vendored editors that slip past the
        // standard exclusion list. Same opt-in shape as the autogen
        // checks — `includeAutogen: true` keeps them. Cheap: only fires
        // on .js/.jsx/.mjs/.cjs/.css/.scss/.sass/.less and reads 4KB.
        if (!opts.includeAutogen && (0, minified_detection_1.isLikelyMinified)(path.join(absDir, ent.name)))
            continue;
        out.push(relPath);
    }
}
function isTestFile(relPath, basename, patterns) {
    for (const pat of patterns.nameOnly) {
        if (matchesBasenameGlob(pat, basename))
            return true;
    }
    for (const pat of patterns.pathAnchored) {
        // Path-anchored patterns like `tests/*.rs` — match against the
        // relative path. Convert glob to regex (path-segment aware).
        if (matchesPathGlob(pat, relPath))
            return true;
    }
    return false;
}
function matchesAnyBasenameGlob(basename, patterns) {
    for (const pat of patterns) {
        if (matchesBasenameGlob(pat, basename))
            return true;
    }
    return false;
}
function matchesBasenameGlob(pat, base) {
    if (!pat.includes('*') && !pat.includes('?'))
        return pat === base;
    const regex = globToRegex(pat, false);
    return regex.test(base);
}
function matchesPathGlob(pat, relPath) {
    // `tests/*.rs` should match `tests/foo.rs` AND `crate/tests/foo.rs`
    // (path-anchored "anywhere in tree"). Mirrors find's `-path "*/x"`.
    const regex = globToRegex(pat, true);
    return regex.test(relPath);
}
function globToRegex(pat, allowSlash) {
    const star = allowSlash ? '.*' : '[^/]*';
    const body = pat
        .replace(/[.+^${}()|[\]\\]/g, '\\$&')
        .replace(/\*/g, star)
        .replace(/\?/g, allowSlash ? '.' : '[^/]');
    // Path-anchored: allow optional leading directory segments.
    return new RegExp(allowSlash ? `(?:^|/)${body}$` : `^${body}$`, 'i');
}
function splitTestPatterns(patterns) {
    const nameOnly = [];
    const pathAnchored = [];
    for (const p of patterns) {
        if (p.includes('/'))
            pathAnchored.push(p);
        else
            nameOnly.push(p);
    }
    return { nameOnly, pathAnchored };
}
/**
 * Count directories under `cwd`, mirroring `find . -type d ${getFindExcludeFlags(cwd)}`:
 * the root counts as 1, plus every descendant directory not pruned by
 * the centralized exclusion set (`isExcludedPath`). Cross-platform,
 * pure-Node replacement for the POSIX `find -type d` shell-out — which
 * returned 0 on Windows (no `find`), zeroing the directory-count metric.
 */
function countDirectories(cwd) {
    let count = 0;
    const visit = (relDir) => {
        count++; // this directory (root included, matching `find .`)
        let entries;
        try {
            entries = fs.readdirSync(relDir ? path.join(cwd, relDir) : cwd, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const ent of entries) {
            if (!ent.isDirectory())
                continue;
            const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
            if ((0, exclusions_1.isExcludedPath)(cwd, rel)) {
                // `find -not -path "*/X/*"` still emits the excluded directory
                // ENTRY (only its contents are filtered out), so count it once
                // but do not descend — matches the prior shell behavior exactly.
                count++;
                continue;
            }
            visit(rel);
        }
    };
    visit('');
    return count;
}
/**
 * Read each file once, scan in-process. Pass `patterns` as compiled
 * regexes for repeated use, or as strings (compiled here with global
 * flag). Each line is tested against the alternation; multiple
 * patterns are unioned via `RegExp` `|` — pass them pre-merged for
 * best performance.
 */
function countLineMatches(cwd, files, patterns, opts = {}) {
    const mode = opts.mode ?? 'lines';
    const skipComments = opts.skipComments ?? false;
    const topN = opts.perFileTopN ?? 0;
    const compiled = compilePatterns(patterns);
    if (compiled.length === 0)
        return { lines: 0, files: 0, perFile: [] };
    let totalLines = 0;
    let totalFiles = 0;
    const perFile = [];
    for (const relPath of files) {
        let content;
        try {
            content = fs.readFileSync(path.join(cwd, relPath), 'utf-8');
        }
        catch {
            continue;
        }
        const commentSyntax = skipComments ? commentSyntaxFor(relPath) : null;
        let fileCount = 0;
        for (const rawLine of content.split('\n')) {
            if (commentSyntax && isCommentLine(rawLine, commentSyntax))
                continue;
            let matched = false;
            for (const re of compiled) {
                // Reset lastIndex on `g` flag patterns to avoid stateful skips.
                re.lastIndex = 0;
                if (re.test(rawLine)) {
                    matched = true;
                    break;
                }
            }
            if (matched)
                fileCount++;
        }
        if (fileCount > 0) {
            totalFiles++;
            totalLines += fileCount;
            if (topN > 0)
                perFile.push({ file: relPath, count: fileCount });
        }
    }
    let topOffenders = [];
    if (topN > 0) {
        perFile.sort((a, b) => b.count - a.count);
        topOffenders = perFile.slice(0, topN);
    }
    return {
        lines: mode === 'lines' ? totalLines : totalFiles,
        files: totalFiles,
        perFile: topOffenders,
    };
}
function compilePatterns(patterns) {
    const out = [];
    for (const p of patterns) {
        if (p instanceof RegExp) {
            out.push(p);
        }
        else if (typeof p === 'string' && p.length > 0) {
            try {
                out.push(new RegExp(p));
            }
            catch {
                // Skip un-compilable patterns rather than throwing — matches
                // grep's behaviour of warning and continuing.
            }
        }
    }
    return out;
}
function commentSyntaxFor(relPath) {
    const ext = path.extname(relPath).toLowerCase();
    switch (ext) {
        case '.ts':
        case '.tsx':
        case '.js':
        case '.jsx':
        case '.mjs':
        case '.cjs':
        case '.go':
        case '.rs':
        case '.java':
        case '.kt':
        case '.kts':
        case '.cs':
        case '.cpp':
        case '.cxx':
        case '.cc':
        case '.c':
        case '.h':
        case '.hpp':
        case '.swift':
        case '.scala':
            return 'slash';
        case '.py':
        case '.rb':
        case '.sh':
        case '.bash':
        case '.zsh':
        case '.yml':
        case '.yaml':
        case '.toml':
        case '.r':
            return 'hash';
        default:
            return 'none';
    }
}
function isCommentLine(rawLine, syntax) {
    if (syntax === 'none')
        return false;
    const trimmed = rawLine.trimStart();
    if (!trimmed)
        return false;
    if (syntax === 'slash') {
        return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*');
    }
    return trimmed.startsWith('#');
}
// ─── Re-exports for test convenience ────────────────────────────────────────
/** Surface the pack-aware `sourceExtensions` for a given packId. Used
 *  when callers want to walk one pack's files but bypass the registry. */
function packSourceExtensions(packId) {
    return (0, languages_1.getLanguage)(packId)?.sourceExtensions ?? [];
}
//# sourceMappingURL=walk-source-files.js.map