diff --git a/package.json b/package.json index 1695439..a99addf 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "build": "npm run bundle:global && npm run bundle:esm && npm run types", "pretest": "npm run build", "test": "jasmine", - "compare": "node spec/compare-oniguruma.js", + "onig:compare": "node scripts/onig-compare.js", + "onig:match": "node scripts/onig-match.js", "prepare": "npm test" }, "files": [ diff --git a/scripts/onig-compare.js b/scripts/onig-compare.js new file mode 100644 index 0000000..b6f5f6e --- /dev/null +++ b/scripts/onig-compare.js @@ -0,0 +1,49 @@ +import {r} from '../src/utils.js'; +import {ansi, areMatchDetailsEqual, err, ok, onigurumaResult, transpiledRegExpResult} from './utils.js'; + +// Help with improving this script or moving it into Jasmine specs would be very welcome + +compare([ + [r`\x7F`, '\x7F'], + [r`\x80`, '\x80'], + [r`\x`, '\\x'], + [r`\p{`, '\\p{'], + [r`\O`, '\n'], + [r`\u{A0}`, '\u{A0}\\u{A0}'] +]); + +async function compare(tests) { + let numSame = 0; + let numDiff = 0; + for (let i = 0; i < tests.length; i++) { + const [pattern, str] = tests[i]; + const lib = transpiledRegExpResult(pattern, str); + const onig = await onigurumaResult(pattern, str); + const searched = `/${pattern}/ with str "${esc(str)}" (len ${str.length})`; + if (areMatchDetailsEqual(lib, onig)) { + numSame++; + ok(i, `Results matched for ${searched}${lib.error ? ` ${ansi.yellow}(both errored)${ansi.reset}` : ''}`); + continue; + } + numDiff++; + if (lib.error) { + err(i, `Only lib errored for ${searched}`); + } else if (onig.error) { + err(i, `Only onig errored for ${searched}`); + } else if (lib.result !== onig.result) { + err(i, `Results differed for ${searched}: lib: ${lib.result && `"${esc(lib.result)}"`}, onig: ${onig.result && `"${esc(onig.result)}"`}`); + } else if (lib.index !== onig.index) { + err(i, `Match positions differed for ${searched}: lib: ${lib.index}, onig: ${onig.index}`); + } + } + numSame &&= `${ansi.green}${numSame}${ansi.reset}`; + numDiff &&= `${ansi.red}${numDiff}${ansi.reset}`; + console.log(`\nFinished: ${numSame} same, ${numDiff} different`); +} + +function esc(str) { + return str. + replace(/\n/g, '\\n'). + replace(/\r/g, '\\r'). + replace(/\0/g, '\\0'); +} diff --git a/scripts/onig-match.js b/scripts/onig-match.js new file mode 100644 index 0000000..15095f9 --- /dev/null +++ b/scripts/onig-match.js @@ -0,0 +1,51 @@ +import {areMatchDetailsEqual, err, ok, onigurumaResult, transpiledRegExpResult} from "./utils.js"; + +exec(process.argv.slice(2)); + +// Basic Oniguruma console-based tester that also does a comparison with Oniguruma-to-ES results +async function exec([pattern, str]) { + if (!(typeof pattern === 'string' && typeof str === 'string')) { + err(null, 'pattern and str args expected'); + return; + } + + const libMatches = []; + let libMatch = transpiledRegExpResult(pattern, str, 0); + while (libMatch.result) { + libMatches.push(libMatch); + libMatch = transpiledRegExpResult(pattern, str, libMatch.index + libMatch.result.length); + } + const onigMatches = []; + let onigMatch = await onigurumaResult(pattern, str, 0); + while (onigMatch.result) { + onigMatches.push(onigMatch); + onigMatch = await onigurumaResult(pattern, str, onigMatch.index + onigMatch.result.length); + } + + console.log('Pattern:', pattern); + console.log('String:', str); + if (onigMatch.error) { + err(null, `Oniguruma error: ${onigMatch.error.message}`); + } else { + console.log('Oniguruma results:', onigMatches); + } + if (!!libMatch.error !== !!onigMatch.error) { + err(null, `Oniguruma and library results differed (only ${libMatch.error ? 'library' : 'Oniguruma'} threw error)`); + } else if (libMatches.length !== onigMatches.length) { + err(null, `Oniguruma and library had different number of results (${onigMatches.length}, ${libMatches.length})`); + } else { + let hasDiff = false; + for (let i = 0; i < libMatches.length; i++) { + if (!areMatchDetailsEqual(libMatches[i], onigMatches[i])) { + hasDiff = true; + break; + } + } + if (hasDiff) { + err(null, 'Oniguruma and library results differed'); + console.log('Library results:', libMatches); + } else { + ok(null, 'Oniguruma and library results matched'); + } + } +} diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..973a6d5 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,127 @@ +import {toRegExp} from '../dist/index.mjs'; +import {readFileSync} from 'node:fs'; +// vscode-oniguruma 2.0.1 uses Oniguruma 6.9.8 +import oniguruma from 'vscode-oniguruma'; + +const ansi = { + green: '\x1b[32m', + red: '\x1b[31m', + reset: '\x1b[0m', + yellow: '\x1b[33m', +}; + +function ok(i, msg) { + console.log(`${i ? ` ${i}. ` : ''}${ansi.green}✅${ansi.reset} ${msg}`); +} + +function err(i, msg) { + console.log(`${i ? ` ${i}. ` : ''}${ansi.red}❌ ${msg}${ansi.reset}`); +} + +/** +@typedef {{ + result: string | null; + index: number | null; + error?: Error; +}} MatchDetails +*/ +/** +@template [T=MatchDetails] +@typedef MatchDetailsFn +@type {{ + (pattern: string, str: string, pos?: number): T; +}} +*/ +/** +@param {RegExpExecArray | null | Error} match +@returns {MatchDetails} +*/ +function getMatchDetails(match) { + if (!match) { + return { + result: null, + index: null, + } + } + if (match instanceof Error) { + return { + result: null, + index: null, + error: match, + }; + } + return { + result: match[0], + index: match.index, + }; +} + +/** +@type {MatchDetailsFn>} +*/ +const onigurumaResult = async (pattern, str, pos) => { + let result; + try { + result = await onigurumaExec(pattern, str, pos); + } catch (err) { + result = err; + } + return getMatchDetails(result); +}; + +/** +@type {MatchDetailsFn} +*/ +const transpiledRegExpResult = (pattern, str, pos) => { + let result; + try { + const options = pos ? {global: true} : undefined; + const re = toRegExp(pattern, '', options); + if (pos) { + re.lastIndex = pos; + } + result = re.exec(str); + } catch (err) { + result = err; + } + return getMatchDetails(result); +}; + +async function onigurumaExec(pattern, str, pos = 0) { + await loadOniguruma(); + // See https://github.com/microsoft/vscode-oniguruma/blob/main/main.d.ts + const re = new oniguruma.OnigScanner([pattern]); + const match = re.findNextMatchSync(str, pos); + if (!match) { + return null; + } + const m = match.captureIndices[0]; + return { + '0': str.slice(m.start, m.end), + index: m.start, + }; +} + +async function loadOniguruma() { + const wasmPath = `${import.meta.dirname}/../node_modules/vscode-oniguruma/release/onig.wasm`; + const wasmBin = readFileSync(wasmPath).buffer; + await oniguruma.loadWASM(wasmBin); +} + +/** +@param {MatchDetails} a +@param {MatchDetails} b +@returns {boolean} +*/ +function areMatchDetailsEqual(a, b) { + return !(a.index !== b.index || a.result !== b.result || !!a.error !== !!b.error); +} + +export { + ansi, + areMatchDetailsEqual, + err, + ok, + onigurumaResult, + transpiledRegExpResult, +}; diff --git a/spec/compare-oniguruma.js b/spec/compare-oniguruma.js deleted file mode 100644 index c99bf0a..0000000 --- a/spec/compare-oniguruma.js +++ /dev/null @@ -1,119 +0,0 @@ -import { error } from 'node:console'; -import {toRegExp} from '../dist/index.mjs'; -import {r} from '../src/utils.js'; -import {readFileSync} from 'node:fs'; -import path from 'node:path'; -import oniguruma from 'vscode-oniguruma'; - -// Help with improving this tester or moving it into Jasmine specs would be very welcome! -// Note: vscode-oniguruma 2.0.1 uses Oniguruma 6.9.8 - -compare([ - [r`\x7F`, '\x7F'], - [r`\x80`, '\x80'], - [r`\x07F`, '\x7F'], - [r`\O`, '\n'], -]); - -async function compare(tests) { - let numSame = 0; - let numDiff = 0; - for (let i = 0; i < tests.length; i++) { - const [pattern, str] = tests[i]; - let libMatch; - let onigMatch; - try { - libMatch = toRegExp(pattern).exec(str); - } catch (err) { - libMatch = err; - } - try { - onigMatch = await onigurumaExec(pattern, str); - } catch (err) { - onigMatch = err; - } - const lib = getDetails(libMatch); - const onig = getDetails(onigMatch); - const searched = `[/${pattern}/ with str "${esc(str)}"]`; - if ((lib.result === onig.result && lib.index === onig.index) || (lib.error && onig.error)) { - numSame++; - ok(i, `Results match ${searched}`); - continue; - } - numDiff++; - if (lib.error) { - err(i, `Only lib errored ${searched}`); - } else if (onig.error) { - err(i, `Only onig errored ${searched}`); - } else if (lib.result !== onig.result) { - err(i, `Results differ ${searched} lib: ${lib.result && `"${esc(lib.result)}"`}, onig: ${onig.result && `"${esc(onig.result)}"`}`); - } else if (lib.index !== onig.index) { - err(i, `Positions differ ${searched} lib: ${lib.index}, onig: ${onig.index}`); - } - } - numSame &&= `${ansi.green}${numSame}${ansi.reset}`; - numDiff &&= `${ansi.red}${numDiff}${ansi.reset}`; - console.log(`\nFinished: ${numSame} same, ${numDiff} different`); -} - -async function loadOniguruma() { - const wasmPath = path.join(import.meta.dirname, '..', 'node_modules', 'vscode-oniguruma', 'release', 'onig.wasm'); - const wasmBin = readFileSync(wasmPath).buffer; - await oniguruma.loadWASM(wasmBin); -} - -async function onigurumaExec(pattern, str) { - await loadOniguruma(); - // See https://github.com/microsoft/vscode-oniguruma/blob/main/main.d.ts - const re = new oniguruma.OnigScanner([pattern]); - const match = re.findNextMatchSync(str, 0); - if (!match) { - return null; - } - const m = match.captureIndices[0]; - return { - '0': str.slice(m.start, m.end), - index: m.start, - }; -} - -function ok(i, msg) { - console.log(` ${i}. ${ansi.green}🆗${ansi.reset} ${msg}`); -} - -function err(i, msg) { - console.log(` ${i}. ${ansi.red}❌ ${msg}${ansi.reset}`); -} - -function esc(str) { - return str. - replace(/\n/g, '\\n'). - replace(/\r/g, '\\r'). - replace(/\0/g, '\\0'); -} - -function getDetails(match) { - if (!match) { - return { - result: null, - index: null, - } - } - if (match instanceof Error) { - return { - result: null, - index: null, - error: match, - }; - } - return { - result: match[0], - index: match.index, - }; -} - -const ansi = { - green: '\x1b[32m', - red: '\x1b[31m', - reset: '\x1b[0m', -};