check / scan: project walks skip dot folders, worktrees and git-ignored output, no stat per entry (244 s -> 46 s on a big game repo)

This commit is contained in:
CRTkafa 2026-10-29 17:29:28 +07:00
parent f2d7beb7ce
commit 164ba390dd
6 changed files with 106 additions and 30 deletions

View file

@ -2,9 +2,10 @@
// .xcframework, SwiftPM checkouts, Capacitor / Cordova / React Native plugins in node_modules, a built .app's
// Frameworks and bundles) and every PrivacyInfo.xcprivacy inside an .ipa or .app (XML or binary). Read-only; the
// directory walk is bounded.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { type Dirent, readdirSync, readFileSync, statSync } from 'node:fs';
import { basename, join, relative, sep } from 'node:path';
import { nativeIosPlugins } from './node-packages';
import { skipDir } from './walk-skip';
import { parseBinaryPlist } from './plist';
import { type Manifest, manifestFromPlist, parseManifest, trackingIssue } from './privacy-manifest';
import { zipEntries, zipRead } from './zip';
@ -63,15 +64,16 @@ export function bundleTrackingIssues(list: BundleManifest[]): string[] {
function walk(dir: string, visit: (p: string, isDir: boolean) => boolean | void, depth = 0, budget = { n: 60_000 }): void {
if (depth > 16 || budget.n <= 0) return;
let names: string[] = [];
try { names = readdirSync(dir).sort(); } catch { return; }
for (const n of names) {
let ents: Dirent[] = [];
try { ents = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); } catch { return; }
for (const e of ents) {
if (--budget.n <= 0) return;
const p = join(dir, n);
let st;
try { st = statSync(p); } catch { continue; }
const descend = visit(p, st.isDirectory());
if (st.isDirectory() && descend !== false) walk(p, visit, depth + 1, budget);
const p = join(dir, e.name);
// the entry type comes with the listing (no stat per file: a stat each made a big tree take minutes on Windows)
let isDir = e.isDirectory();
if (e.isSymbolicLink()) { try { isDir = statSync(p).isDirectory(); } catch { continue; } }
const descend = visit(p, isDir);
if (isDir && descend !== false) walk(p, visit, depth + 1, budget);
}
}
@ -102,7 +104,7 @@ export function projectSdkManifests(root: string): SdkManifest[] {
try { out.push({ sdk, manifest: manifestFromBytes(relative(root, abs).split(sep).join('/'), readFileSync(abs)) }); } catch { /* unreadable */ }
};
walk(root, (p, isDir) => {
if (isDir) return !SKIP.has(basename(p));
if (isDir) return !SKIP.has(basename(p)) && !skipDir(root, p, basename(p));
if (basename(p) !== 'PrivacyInfo.xcprivacy') return;
const sdk = sdkOfProjectPath(relative(root, p).split(sep).join('/'));
if (sdk) add(p, sdk);