Created
August 26, 2026 19:01
-
-
Save billywhizz/c891b157c87b44dc3591a1089655b35e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // v8-deprecation-scan.js [--lo-dir <path>] | |
| // | |
| // Runs under either `node` or `lo` (verified against real output from | |
| // both - see V8.md section 9 for the diff). Automates the *mechanical* | |
| // half of that sweep: fetches V8 mainline's real embedder headers, | |
| // extracts every V8_DEPRECATED/V8_DEPRECATE_SOON-tagged declaration, | |
| // then greps <lo-dir> (default: ../repos/lo relative to this script) | |
| // for method calls matching those names. Prints a candidate report to | |
| // stdout - each candidate includes the real call site as a markdown | |
| // link to the exact line on GitHub (pinned to <lo-dir>'s checked-out | |
| // commit, not a moving branch), a link to the deprecated declaration in | |
| // V8's own source (pinned to the commit its headers were fetched at), | |
| // and a link to any crbug/bugs.chromium.org tracking bug mentioned in | |
| // V8's own comment near the tag, when there is one. | |
| // | |
| // Deliberately does NOT try to fully automate V8.md section 9's table. | |
| // Two things a script can't do reliably, left as a manual/AI follow-up | |
| // pass on this report's output: | |
| // 1. Confirm a name match is the *same overload/class*, not a | |
| // same-named method on something unrelated (e.g. this session's | |
| // Script::GetId() vs StackFrame::GetScriptId() false positive - | |
| // only caught by reading the actual call site's receiver type). | |
| // 2. Real-world removal-timeline evidence (e.g. finding that Node.js | |
| // already migrated off a given API years ago and it's *still* not | |
| // removed upstream) - that needs web research, not grep. | |
| // | |
| // Runtime notes for the `lo` path (verified empirically against | |
| // /root/lo, not assumed from docs - see V8.md section 9's dual-runtime | |
| // note for what was actually tested): | |
| // - No global `fetch()`/`Buffer` under `lo`. Network fetch uses | |
| // `lib/fs.js`/`lib/curl.js` - both **builtins**, `.incbin`-linked | |
| // into the binary (`lo.builtins()` lists them), so they resolve as | |
| // plain bare `import ... from 'lib/curl.js'` specifiers from *any* | |
| // CWD, no filesystem dependency on a repos/lo checkout at all. | |
| // `lib/curl.js`'s `fetch_fd(url, fd)` writes straight into an | |
| // in-memory fd (`core.memfd_create` - no temp file, per direct user | |
| // suggestion) - but note it sets `CURLOPT_HEADER=1`, so the raw HTTP | |
| // response header block(s) land in the same stream ahead of the | |
| // body and need stripping (one block per redirect hop) - confirmed | |
| // by testing against a real URL, this wasn't obvious from the | |
| // source alone. Base64 (gitiles `?format=TEXT`) is decoded with a | |
| // small hand-rolled decoder used uniformly by *both* runtimes | |
| // (simpler than branching Buffer vs. a lo-only decoder). | |
| // - `fetch_fd`'s returned `size` (`CURLINFO_SIZE_DOWNLOAD_T`) is the | |
| // response *body* size only - it never counts the extra header | |
| // bytes `CURLOPT_HEADER=1` (see above) adds to the same stream, on | |
| // top of the body. Trusting it truncated a real JSON response | |
| // mid-string in testing; first guess was a gzip-decompression | |
| // effect, which was wrong - confirmed the real, exact mechanism | |
| // with `lib/pico.js`'s real HTTP parser instead of guessing further | |
| // (a real response had 1329 header bytes + 11910 reported body | |
| // bytes = 13239 total, matching `fstat` precisely). `fetchRawText` | |
| // below `fstat`s the fd for the real total instead of trusting the | |
| // return value - see PLAN.md task 49 bug 3 for whether the real fix | |
| // belongs in `lib/curl.js` itself. | |
| // - `lo.core.readFile`/`writeFile` can hard-crash the process (real, | |
| // documented, uncatchable V8 OOM abort - see LO.md) if called with | |
| // bare-string Node-`fs`-shaped arguments. File reads here always go | |
| // through the raw `open`/`fstat`/`read`/`close` sequence instead, | |
| // matching `lib/fs.js`'s own established-safe pattern - confirmed | |
| // empirically against a real file before trusting it, not assumed. | |
| // - `lib/proc.js`'s `exec()` has a real, documented bug (missing | |
| // `lo.exit()` after a failed `execvp` in the forked child re-runs | |
| // the whole calling script a second time as an orphaned duplicate - | |
| // see OPENVSCODE-SERVER-PLAN.md). Sidestepped entirely: git | |
| // metadata is read directly from `.git/HEAD`/`.git/config`/ | |
| // `.git/refs` (plain file reads, real git plumbing, no subprocess) | |
| // instead of shelling out to `git rev-parse`/`git remote`. If a | |
| // future script genuinely needs subprocess output capture, | |
| // `lib/pmon.js`'s `Process` class is the safer option (real pipes + | |
| // it actually calls `lo.exit()` on a failed exec, unlike | |
| // `lib/proc.js`) - not needed here, noted for next time. | |
| // - Directory walking uses `lib/fs.js`'s builtin `readdir_sync` | |
| // (recursive by default, unlike Node's) rather than re-deriving its | |
| // OS-specific dirent-parsing offset math by hand. | |
| // - `console.log`/`console.error` under `lo` are single-parameter | |
| // functions - extra arguments are silently dropped (confirmed | |
| // directly in `main.js`: `log: str => write_string(STDOUT, | |
| // \`${str}\n\`)`). Every call in this file passes exactly one | |
| // (template-literal) argument, which is also perfectly valid Node | |
| // style - no branching needed for this one. | |
| // - `LO_HOME` (used elsewhere in this repo to point `lo` at a | |
| // checkout for its own non-builtin `lib/*.js` resolution) isn't | |
| // needed here - both modules used are builtins, not read from | |
| // `--lo-dir` at all; `--lo-dir` is purely the directory this script | |
| // scans, unrelated to module resolution. | |
| const isNode = !!globalThis.process?.versions?.node | |
| const HEADERS = [ | |
| 'v8-object.h', 'v8-value.h', 'v8-function.h', 'v8-context.h', | |
| 'v8-isolate.h', 'v8-primitive.h', 'v8-array-buffer.h', 'v8-script.h', | |
| 'v8-microtask-queue.h', 'v8-exception.h', 'v8-data.h', | |
| 'v8-primitive-object.h', 'v8-promise.h' | |
| ] | |
| const HEADER_BASE = 'https://chromium.googlesource.com/v8/v8/+/refs/heads/main/include/' | |
| // ---------- universal helpers (no runtime-specific APIs) ---------- | |
| const B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' | |
| function base64Decode (b64) { | |
| const clean = b64.replace(/[\r\n\s]/g, '').replace(/=+$/, '') | |
| const bytes = [] | |
| let buffer = 0 | |
| let bits = 0 | |
| for (let i = 0; i < clean.length; i++) { | |
| const idx = B64_CHARS.indexOf(clean[i]) | |
| if (idx === -1) continue | |
| buffer = (buffer << 6) | idx | |
| bits += 6 | |
| if (bits >= 8) { | |
| bits -= 8 | |
| bytes.push((buffer >> bits) & 0xff) | |
| } | |
| } | |
| return new Uint8Array(bytes) | |
| } | |
| function relPath (from, to) { | |
| return to.startsWith(from + '/') ? to.slice(from.length + 1) : to | |
| } | |
| // Collapses `.`/`..`/`//` segments in a POSIX path string. Needed | |
| // because LO_DIR is built with a literal `..` (scriptDir/../repos/lo) - | |
| // Node's path.join later normalizes that away when building call-site | |
| // paths, but our own manual string joins (both runtimes' listSourceFiles, | |
| // and relPath's prefix match) don't - normalize once here, at the one | |
| // place `..` gets introduced, so every path built *from* LO_DIR downstream | |
| // stays consistent regardless of runtime, instead of diverging. | |
| function normalizePath (p) { | |
| const abs = p.startsWith('/') | |
| const parts = p.split('/').filter(s => s && s !== '.') | |
| const out = [] | |
| for (const part of parts) { | |
| if (part === '..' && out.length && out[out.length - 1] !== '..') out.pop() | |
| else out.push(part) | |
| } | |
| return (abs ? '/' : '') + out.join('/') | |
| } | |
| // ---------- argv / script location ---------- | |
| const argv = isNode ? process.argv.slice(2) : lo.args.slice(2) | |
| const loDirFlagIdx = argv.indexOf('--lo-dir') | |
| // __dirname doesn't exist in real ESM (this file has top-level await, so | |
| // Node parses it as ESM, not CommonJS) - derive it from import.meta.url | |
| // instead, same as bindings/gen-loader.js does. | |
| const scriptDir = isNode | |
| ? new URL('.', import.meta.url).pathname.replace(/\/$/, '') | |
| : lo.args[1].split('/').slice(0, -1).join('/') | |
| const LO_DIR = normalizePath(loDirFlagIdx !== -1 && argv[loDirFlagIdx + 1] | |
| ? argv[loDirFlagIdx + 1] | |
| : `${scriptDir}/../repos/lo`) | |
| // ---------- runtime-specific I/O shims ---------- | |
| // Each runtime fills in: fetchRawText, readFileText, listSourceFiles, | |
| // fatalExit. Everything below this block is runtime-agnostic. | |
| let fetchRawText, readFileText, listSourceFiles, fatalExit | |
| if (isNode) { | |
| // dynamic import, not require() - this file also has top-level await | |
| // (the lo branch below), and Node can't parse a file as CommonJS | |
| // (require) and ESM (top-level await) at once - matches | |
| // bindings/gen-module.js's own established style for exactly this | |
| // reason. | |
| const fs = await import('node:fs') | |
| const path = await import('node:path') | |
| fetchRawText = async (url) => { | |
| const res = await fetch(url) | |
| if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`) | |
| return res.text() | |
| } | |
| readFileText = (p) => fs.readFileSync(p, 'utf8') | |
| listSourceFiles = (dir) => { | |
| const results = [] | |
| const skip = new Set(['v8', '.git', 'node_modules']) | |
| function walk (d) { | |
| let entries | |
| try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { return } | |
| for (const e of entries) { | |
| if (skip.has(e.name)) continue | |
| const full = path.join(d, e.name) | |
| if (e.isDirectory()) walk(full) | |
| else if (e.name.endsWith('.cc') || e.name === 'api.js') results.push(full) | |
| } | |
| } | |
| walk(dir) | |
| // deterministic order - Node's readdirSync and lo's readdir_sync | |
| // enumerate directory entries differently (alphabetical vs. raw | |
| // dirent/inode order), which otherwise reorders (not loses) call | |
| // sites between runtimes - confirmed by diffing real output from | |
| // both before adding this. | |
| results.sort() | |
| return results | |
| } | |
| fatalExit = (msg) => { console.error(msg); process.exit(1) } | |
| } else { | |
| // lo runtime - see the header comment above for why each piece here | |
| // is shaped the way it is (all verified empirically, not guessed). | |
| // Both imports are builtins (lo.builtins() lists them, .incbin-linked | |
| // into the binary) - plain bare specifiers, no filesystem/CWD | |
| // dependency on any repos/lo checkout. | |
| const { readdir_sync } = await import('lib/fs.js') | |
| const { fetch_fd } = await import('lib/curl.js') | |
| const { ResponseParser } = await import('lib/pico.js') | |
| const { assert, ptr, utf8Decode, core } = lo | |
| const { memfd_create, lseek, read: coreRead, close: coreClose, open: coreOpen, fstat, O_RDONLY } = core | |
| const SEEK_SET = 0 | |
| // fetch_fd sets CURLOPT_HEADER=1 (fixed upstream in this session's own | |
| // repos/lo/lib/curl.js edit, PLAN.md task 49 bug 1 - but this script | |
| // should keep working against a not-yet-rebuilt `lo` binary too), so | |
| // the raw HTTP response header block(s) - one per redirect hop - land | |
| // in the stream ahead of the body. Skip them with a real HTTP parser | |
| // (lib/pico.js's ResponseParser, per direct user suggestion) rather | |
| // than a hand-rolled regex - more correct (protocol-aware, exact byte | |
| // count) and it's what caught the *actual* mechanism of the size bug | |
| // below (a regex-based guess at "gzip decompression" was wrong). | |
| function skipHttpHeaders (buf, size) { | |
| let offset = 0 | |
| let hops = 0 | |
| const parser = new ResponseParser(buf, 32) | |
| while (hops < 5) { | |
| const consumed = parser.parse(size - offset) | |
| if (consumed <= 0) break | |
| offset += consumed | |
| hops++ | |
| if (parser.status < 300 || parser.status >= 400) break | |
| parser.rb = buf.subarray(offset) | |
| } | |
| return offset | |
| } | |
| fetchRawText = (url) => { | |
| const fd = memfd_create('v8-deprecation-scan', 0) | |
| assert(fd > 0, 'memfd_create failed') | |
| fetch_fd(url, fd, 200) | |
| // Real bug, confirmed precisely (not just worked around) using | |
| // ResponseParser above: fetch_fd's returned `size` | |
| // (CURLINFO_SIZE_DOWNLOAD_T) is the response *body* size only - it | |
| // never counted the extra header bytes CURLOPT_HEADER=1 adds to the | |
| // same stream, on top of the body. Confirmed exactly: a real | |
| // response had 1329 header bytes + 11910 body bytes (the reported | |
| // `size`) = 13239 total, matching `fstat` precisely - not a | |
| // gzip/compression effect as first (wrongly) guessed, just headers | |
| // never being counted. fstat the fd for the real total instead of | |
| // trusting the returned size, same safe-size pattern as | |
| // readFileText below. | |
| const statBuf = ptr(new Uint8Array(160)) | |
| assert(fstat(fd, statBuf.ptr) === 0) | |
| const size = Number(new BigUint64Array(statBuf.buffer)[6]) | |
| assert(lseek(fd, 0, SEEK_SET) === 0) | |
| const buf = ptr(new Uint8Array(size)) | |
| const n = coreRead(fd, buf.ptr, size) | |
| coreClose(fd) | |
| const bodyOffset = skipHttpHeaders(buf, n) | |
| return utf8Decode(buf.ptr + bodyOffset, n - bodyOffset) | |
| } | |
| readFileText = (p) => { | |
| const fd = coreOpen(p, O_RDONLY) | |
| if (fd <= 0) throw new Error(`could not open ${p}`) | |
| const statBuf = ptr(new Uint8Array(160)) | |
| assert(fstat(fd, statBuf.ptr) === 0) | |
| const size = Number(new BigUint64Array(statBuf.buffer)[6]) | |
| const buf = ptr(new Uint8Array(size)) | |
| const n = coreRead(fd, buf.ptr, size) | |
| coreClose(fd) | |
| return utf8Decode(buf.ptr, n) | |
| } | |
| listSourceFiles = (dir) => { | |
| const skip = new Set(['v8', '.git', 'node_modules']) | |
| const results = [] | |
| function walk (d) { | |
| let entries | |
| try { entries = readdir_sync(d, [], { recursive: false }) } catch { return } | |
| for (const e of entries) { | |
| if (skip.has(e.name)) continue | |
| if (e.isDirectory) walk(`${d}/${e.name}`) | |
| else if (e.isFile && (e.name.endsWith('.cc') || e.name === 'api.js')) { | |
| results.push(`${d}/${e.name}`) | |
| } | |
| } | |
| } | |
| walk(dir) | |
| // deterministic order - Node's readdirSync and lo's readdir_sync | |
| // enumerate directory entries differently (alphabetical vs. raw | |
| // dirent/inode order), which otherwise reorders (not loses) call | |
| // sites between runtimes - confirmed by diffing real output from | |
| // both before adding this. | |
| results.sort() | |
| return results | |
| } | |
| fatalExit = (msg) => { console.error(msg); lo.exit(1) } | |
| } | |
| // ---------- git plumbing, no subprocess (universal - see header comment) ---------- | |
| function resolveGithubOrigin (dir) { | |
| const fallback = { repo: 'just-js/lo', ref: 'main' } | |
| try { | |
| const config = readFileText(`${dir}/.git/config`) | |
| const remoteBlock = config.match(/\[remote "origin"\]([^[]*)/) | |
| const urlMatch = remoteBlock && remoteBlock[1].match(/url\s*=\s*(\S+)/) | |
| const remote = urlMatch ? urlMatch[1] : '' | |
| const m = remote.match(/github\.com[:/]([^/]+\/[^/.]+)(\.git)?\/?$/) | |
| const repo = m ? m[1] : fallback.repo | |
| const head = readFileText(`${dir}/.git/HEAD`).trim() | |
| let ref = fallback.ref | |
| const refMatch = head.match(/^ref:\s*(\S+)/) | |
| if (refMatch) { | |
| try { | |
| ref = readFileText(`${dir}/.git/${refMatch[1]}`).trim() | |
| } catch { | |
| const packed = readFileText(`${dir}/.git/packed-refs`) | |
| const line = packed.split('\n').find(l => l.endsWith(` ${refMatch[1]}`)) | |
| if (line) ref = line.split(' ')[0] | |
| } | |
| } else if (/^[0-9a-f]{40}$/.test(head)) { | |
| ref = head | |
| } | |
| return { repo, ref } | |
| } catch { | |
| return fallback | |
| } | |
| } | |
| async function resolveV8Ref () { | |
| try { | |
| const body = await fetchRawText('https://api.github.com/repos/v8/v8/commits/main') | |
| const data = JSON.parse(body) | |
| return data.sha || 'main' | |
| } catch { | |
| return 'main' | |
| } | |
| } | |
| function githubLink (repo, ref, relFile, line) { | |
| return `https://github.com/${repo}/blob/${ref}/${relFile}#L${line}` | |
| } | |
| function v8SourceLink (v8Ref, header, line) { | |
| return `https://github.com/v8/v8/blob/${v8Ref}/include/${header}#L${line}` | |
| } | |
| function crbugLink (id) { | |
| return `https://crbug.com/${id}` | |
| } | |
| // ---------- header parsing ---------- | |
| async function fetchHeader (name) { | |
| const b64 = await fetchRawText(`${HEADER_BASE}${name}?format=TEXT`) | |
| const bytes = base64Decode(b64) | |
| return new TextDecoder().decode(bytes) | |
| } | |
| function findCrbug (lines, i) { | |
| const contextStart = Math.max(0, i - 15) | |
| const contextText = lines.slice(contextStart, i).join('\n') | |
| const crbugMatch = contextText.match(/crbug\.com\/(\d+)|bugs\.chromium\.org\/p\/chromium\/issues\/detail\?id=(\d+)/) | |
| return crbugMatch ? (crbugMatch[1] || crbugMatch[2]) : null | |
| } | |
| // C++ keywords/primitive types that can appear immediately before a `(` | |
| // without being the name of anything - a bare cast or a function-pointer | |
| // type's parameter list (e.g. `void (*)(...)`), not a declared identifier. | |
| // Guards the method-style fallback below: without this, a `using Name | |
| // V8_DEPRECATE_SOON(...) = void (*)(...)` alias (real case: | |
| // AccessorNameSetterCallback in v8-object.h) mis-extracts "void" as the | |
| // deprecated name, and a bare \bvoid\b call-site match then matches | |
| // nearly every function in a C++ file - confirmed live, this exact shape | |
| // produced ~30 garbage "call sites" before this guard was added. | |
| const CPP_NON_NAME_WORDS = new Set([ | |
| 'void', 'int', 'bool', 'char', 'double', 'float', 'auto', 'const', | |
| 'static', 'unsigned', 'signed', 'long', 'short' | |
| ]) | |
| // Extracts every V8_DEPRECATED(...)/V8_DEPRECATE_SOON(...) tag, plus any | |
| // crbug/bugs.chromium.org reference in the ~15 lines above the tag. Three | |
| // declaration shapes, checked in this order: | |
| // 1. Enum-style: `<name> V8_DEPRECATED(...) = <value>,` - the tagged | |
| // name sits on the tag's own line, *before* the tag (confirmed | |
| // against the real PromiseRejectEvent declaration in | |
| // include/v8-promise.h - this session's own kPromiseRejectAfterResolved | |
| // miss was exactly this shape going unrecognized). | |
| // 2. Alias-style: `using <Name> // \n V8_DEPRECATE_SOON(...) = <type>;` | |
| // - the name is on a line *before* the tag, not after (real case: | |
| // AccessorNameSetterCallback in v8-object.h). | |
| // 3. Method-style: the tag stands alone (or wraps a multi-line reason | |
| // string) and the declaration - and the name to extract - follows on | |
| // a later line, e.g. GetEmbedderData/SetEmbedderData. | |
| function extractDeprecations (source, headerName) { | |
| const lines = source.split('\n') | |
| const out = [] | |
| for (let i = 0; i < lines.length; i++) { | |
| const m = lines[i].match(/V8_(DEPRECATED|DEPRECATE_SOON)\(/) | |
| if (!m) continue | |
| const tier = m[1] | |
| let tagText = lines[i] | |
| let j = i | |
| while (!tagText.includes(')') && j - i < 4) { | |
| j++ | |
| tagText += ' ' + lines[j] | |
| } | |
| // reason strings are sometimes split across adjacent C++ string | |
| // literals (compiler-concatenated) - join every quoted segment. | |
| const reasonParts = [...tagText.matchAll(/"([^"]*)"/g)].map(x => x[1]) | |
| const reason = reasonParts.length ? reasonParts.join('') : '(no reason string)' | |
| const inlineMatch = lines[i].match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+V8_(DEPRECATED|DEPRECATE_SOON)\(/) | |
| if (inlineMatch) { | |
| out.push({ | |
| method: inlineMatch[1], | |
| reason, | |
| tier, | |
| header: headerName, | |
| headerLine: i + 1, | |
| snippet: lines[i].trim(), | |
| crbugId: findCrbug(lines, i) | |
| }) | |
| continue | |
| } | |
| const usingMatch = (lines[i - 1] || '').match(/^\s*using\s+([A-Za-z_][A-Za-z0-9_]*)\b/) || | |
| (lines[i - 2] || '').match(/^\s*using\s+([A-Za-z_][A-Za-z0-9_]*)\b/) | |
| if (usingMatch) { | |
| out.push({ | |
| method: usingMatch[1], | |
| reason, | |
| tier, | |
| header: headerName, | |
| headerLine: i + 1, | |
| snippet: lines[i].trim(), | |
| crbugId: findCrbug(lines, i) | |
| }) | |
| continue | |
| } | |
| // Method-style fallback: scan forward for the first identifier-then- | |
| // paren that isn't just a keyword/type (see CPP_NON_NAME_WORDS above), | |
| // giving up once the declaration's own `;` is reached. | |
| let k = j + 1 | |
| let methodMatch = null | |
| while (k < lines.length) { | |
| while (k < lines.length && lines[k].trim() === '') k++ | |
| const candidate = (lines[k] || '').match(/([A-Za-z_][A-Za-z0-9_]*)\s*\(/) | |
| if (candidate && !CPP_NON_NAME_WORDS.has(candidate[1])) { | |
| methodMatch = candidate | |
| break | |
| } | |
| if ((lines[k] || '').includes(';')) break | |
| k++ | |
| } | |
| if (!methodMatch) continue | |
| out.push({ | |
| method: methodMatch[1], | |
| reason, | |
| tier, | |
| header: headerName, | |
| headerLine: k + 1, | |
| snippet: (lines[k] || '').trim(), | |
| crbugId: findCrbug(lines, i) | |
| }) | |
| } | |
| return out | |
| } | |
| // Bare \bname\b (not just `(->|\.)name(`) so this also catches deprecated | |
| // *values* - enum constants, static constexpr members - used as plain | |
| // identifiers (`using v8::kPromiseRejectAfterResolved;`, | |
| // `data.GetEvent() == kPromiseRejectAfterResolved`), not just method | |
| // calls. Subsumes the old method-call-only pattern: `\b` already matches | |
| // at the `>`/`.` boundary a call site like `foo->methodName(` has. | |
| function findCallSites (files, methodName) { | |
| const hits = [] | |
| const re = new RegExp(`\\b${methodName}\\b`) | |
| for (const f of files) { | |
| const lines = readFileText(f).split('\n') | |
| lines.forEach((line, idx) => { | |
| if (re.test(line)) hits.push({ file: f, line: idx + 1, text: line.trim() }) | |
| }) | |
| } | |
| return hits | |
| } | |
| // ---------- main ---------- | |
| async function main () { | |
| console.error(`Fetching ${HEADERS.length} V8 headers from chromium.googlesource.com...`) | |
| const deprecations = [] | |
| for (const h of HEADERS) { | |
| try { | |
| const src = await fetchHeader(h) | |
| deprecations.push(...extractDeprecations(src, h)) | |
| } catch (e) { | |
| console.error(` skip ${h}: ${e.message}`) | |
| } | |
| } | |
| console.error(`Found ${deprecations.length} deprecated declarations across fetched headers.`) | |
| let dirExists = true | |
| try { readFileText(`${LO_DIR}/lo.cc`) } catch { dirExists = false } | |
| if (!dirExists) { | |
| fatalExit(`lo dir not found (or missing lo.cc): ${LO_DIR} (pass --lo-dir <path>)`) | |
| return | |
| } | |
| const sourceFiles = listSourceFiles(LO_DIR) | |
| console.error(`Scanning ${sourceFiles.length} .cc/api.js files under ${LO_DIR}...`) | |
| const origin = resolveGithubOrigin(LO_DIR) | |
| console.error(`Linking call sites to https://github.com/${origin.repo}/blob/${origin.ref}/...`) | |
| const v8Ref = await resolveV8Ref() | |
| console.error(`Linking V8 source to https://github.com/v8/v8/blob/${v8Ref}/...`) | |
| const candidates = [] | |
| for (const dep of deprecations) { | |
| const hits = findCallSites(sourceFiles, dep.method) | |
| if (hits.length) candidates.push({ ...dep, hits }) | |
| } | |
| console.log(`# V8 deprecation scan — ${new Date().toISOString().slice(0, 10)} (${isNode ? 'node' : 'lo'})`) | |
| console.log('') | |
| console.log(`Fetched ${HEADERS.length} headers, found ${deprecations.length} deprecated`) | |
| console.log(`declarations, ${candidates.length} have a name match in ${LO_DIR}.`) | |
| console.log('') | |
| console.log('**Candidates only — verify each before trusting it.** A name match can') | |
| console.log('be a false positive (same method name, different class/overload) -') | |
| console.log('check the call site below actually matches the deprecated signature.') | |
| console.log('No risk/timeline assessment here; see V8.md section 9 for that layer.') | |
| console.log('') | |
| if (!candidates.length) { | |
| console.log('No candidates found.') | |
| return | |
| } | |
| for (const c of candidates) { | |
| const srcLink = v8SourceLink(v8Ref, c.header, c.headerLine) | |
| console.log(`## \`${c.method}\` — ${c.tier} ([${c.header}:${c.headerLine}](${srcLink}))`) | |
| console.log('') | |
| console.log(`Reason: ${c.reason}`) | |
| if (c.crbugId) { | |
| console.log('') | |
| console.log(`Tracking bug: [crbug.com/${c.crbugId}](${crbugLink(c.crbugId)})`) | |
| } | |
| console.log('') | |
| console.log(`Declaration: \`${c.snippet}\` ([source](${srcLink}))`) | |
| console.log('') | |
| console.log('Call sites:') | |
| for (const hit of c.hits) { | |
| const rel = relPath(LO_DIR, hit.file) | |
| const link = githubLink(origin.repo, origin.ref, rel, hit.line) | |
| console.log(`- [\`${rel}:${hit.line}\`](${link}) — \`${hit.text}\``) | |
| } | |
| console.log('') | |
| } | |
| } | |
| main().catch(e => fatalExit(`${e.stack || e}`)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment