|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// Guard: the two WebAssembly.instantiate overloads are not interchangeable. |
| 3 | +// |
| 4 | +// WebAssembly.instantiate(BufferSource, imports) -> { module, instance } |
| 5 | +// WebAssembly.instantiate(Module, imports) -> Instance |
| 6 | +// |
| 7 | +// So `.instance` (or `const { instance } = ...`) is correct ONLY for the |
| 8 | +// BufferSource form. Applied to the Module form it is `undefined`, which is |
| 9 | +// how tests/codegen/test_dom_pilot_*.mjs and test_wasi_fs_combo.mjs crashed |
| 10 | +// with `TypeError: Cannot read properties of undefined (reading 'exports')`. |
| 11 | +// |
| 12 | +// Usage: node tools/check-wasm-harness-idioms.mjs [root] (root defaults to .) |
| 13 | +import { readFileSync, readdirSync, statSync } from 'node:fs'; |
| 14 | +import { join, relative, sep } from 'node:path'; |
| 15 | + |
| 16 | +const ROOT = process.argv[2] ?? '.'; |
| 17 | +const MODULE_DECL = |
| 18 | + /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+WebAssembly\.Module\s*\(/g; |
| 19 | + |
| 20 | +function* walkMjs(dir) { |
| 21 | + let entries; |
| 22 | + try { |
| 23 | + entries = readdirSync(dir, { withFileTypes: true }); |
| 24 | + } catch { |
| 25 | + return; |
| 26 | + } |
| 27 | + for (const e of entries) { |
| 28 | + const p = join(dir, e.name); |
| 29 | + if (e.isDirectory()) yield* walkMjs(p); |
| 30 | + else if (e.isFile() && e.name.endsWith('.mjs')) yield p; |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +function matchingParen(src, openIdx) { |
| 35 | + let depth = 0; |
| 36 | + for (let i = openIdx; i < src.length; i++) { |
| 37 | + const c = src[i]; |
| 38 | + if (c === '(') depth++; |
| 39 | + else if (c === ')') { |
| 40 | + depth--; |
| 41 | + if (depth === 0) return i; |
| 42 | + } |
| 43 | + } |
| 44 | + return -1; |
| 45 | +} |
| 46 | + |
| 47 | +const lineOf = (src, idx) => src.slice(0, idx).split('\n').length; |
| 48 | + |
| 49 | +const problems = []; |
| 50 | +const scanned = { files: 0, withModule: 0 }; |
| 51 | + |
| 52 | +for (const file of walkMjs(join(ROOT, 'tests'))) { |
| 53 | + scanned.files++; |
| 54 | + const src = readFileSync(file, 'utf8'); |
| 55 | + const moduleVars = new Set(); |
| 56 | + for (const m of src.matchAll(MODULE_DECL)) moduleVars.add(m[1]); |
| 57 | + if (moduleVars.size === 0) continue; |
| 58 | + scanned.withModule++; |
| 59 | + const rel = relative(ROOT, file).split(sep).join('/'); |
| 60 | + |
| 61 | + for (const v of moduleVars) { |
| 62 | + const callRe = new RegExp( |
| 63 | + `WebAssembly\\.instantiate\\(\\s*${v.replace(/\$/g, '\\$')}\\b`, |
| 64 | + 'g', |
| 65 | + ); |
| 66 | + for (const m of src.matchAll(callRe)) { |
| 67 | + const openIdx = src.indexOf('(', m.index + 'WebAssembly.instantiate'.length); |
| 68 | + const closeIdx = matchingParen(src, openIdx); |
| 69 | + if (closeIdx < 0) continue; |
| 70 | + const after = src.slice(closeIdx + 1, closeIdx + 40); |
| 71 | + const line = lineOf(src, m.index); |
| 72 | + if (/^\s*\)?\s*\.\s*instance\b/.test(after)) { |
| 73 | + problems.push( |
| 74 | + `${rel}:${line}: WebAssembly.instantiate(${v}, …) is the Module overload ` + |
| 75 | + `(resolves to an Instance), so a trailing \`.instance\` is undefined`, |
| 76 | + ); |
| 77 | + continue; |
| 78 | + } |
| 79 | + // Reverse case: destructuring `{ instance }` out of the Module overload. |
| 80 | + const stmtStart = Math.max( |
| 81 | + src.lastIndexOf(';', m.index), |
| 82 | + src.lastIndexOf('\n', m.index), |
| 83 | + 0, |
| 84 | + ); |
| 85 | + const head = src.slice(stmtStart, m.index); |
| 86 | + if (/\{\s*instance\s*(?::\s*[\w$]+\s*)?\}\s*=\s*(?:await\s*)?$/.test(head)) { |
| 87 | + problems.push( |
| 88 | + `${rel}:${line}: \`const { instance } = await WebAssembly.instantiate(${v}, …)\` ` + |
| 89 | + `destructures the BufferSource result shape, not the Module one`, |
| 90 | + ); |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +if (problems.length > 0) { |
| 97 | + console.error('WASM harness instantiation-idiom gate: FAILED\n'); |
| 98 | + for (const p of problems) console.error(' ' + p); |
| 99 | + console.error( |
| 100 | + `\n${problems.length} problem(s) in ${scanned.withModule} module-creating harness(es).` + |
| 101 | + '\nFix: with a WebAssembly.Module argument, use the returned value directly;' + |
| 102 | + '\n with a BufferSource argument, destructure `{ instance }`.' + |
| 103 | + '\nSee tools/check-wasm-harness-idioms.mjs for the full explanation.', |
| 104 | + ); |
| 105 | + process.exit(1); |
| 106 | +} |
| 107 | + |
| 108 | +console.log( |
| 109 | + `WASM harness instantiation-idiom gate: OK ` + |
| 110 | + `(${scanned.files} harnesses scanned, ${scanned.withModule} create a WebAssembly.Module)`, |
| 111 | +); |
0 commit comments