import { multiple as getFixtures } from "@babel/helper-fixtures"; import { codeFrameColumns } from "@babel/code-frame"; import fs from "fs"; import path from "path"; const rootPath = path.join(__dirname, "../../../.."); const serialized = "$$ babel internal serialized type"; class FixtureError extends Error { constructor(previousError, fixturePath, code) { super(previousError.message); const messageLines = (previousError.message.match(/\n/g) || []).length + 1; let fixtureStackFrame = ""; if (previousError.loc) { fixtureStackFrame = codeFrameColumns( code, { start: { line: previousError.loc.line, column: previousError.loc.column + 1, }, }, { highlightCode: true }, ) + "\n" + `at fixture (${fixturePath}:${previousError.loc.line}:${ previousError.loc.column + 1 })\n`; } this.stack = previousError.constructor.name + ": " + previousError.message + "\n" + fixtureStackFrame + previousError.stack.split("\n").slice(messageLines).join("\n"); } } export function runFixtureTests(fixturesPath, parseFunction) { const fixtures = getFixtures(fixturesPath); Object.keys(fixtures).forEach(function (name) { fixtures[name].forEach(function (testSuite) { testSuite.tests.forEach(function (task) { const testFn = task.disabled ? it.skip : it; testFn(name + "/" + testSuite.title + "/" + task.title, function () { try { runTest(task, parseFunction); } catch (err) { if (!task.expect.code && !process.env.CI) { const fn = path.dirname(task.expect.loc) + "/options.json"; if (!fs.existsSync(fn)) { task.options = task.options || {}; task.options.throws = err.message.replace( /^.*Got error message: /, "", ); fs.writeFileSync(fn, JSON.stringify(task.options, null, 2)); } } const fixturePath = `${path.relative( rootPath, fixturesPath, )}/${name}/${task.actual.filename}`; throw new FixtureError(err, fixturePath, task.actual.code); } }); }); }); }); } export function runThrowTestsWithEstree(fixturesPath, parseFunction) { const fixtures = getFixtures(fixturesPath); Object.keys(fixtures).forEach(function (name) { fixtures[name].forEach(function (testSuite) { testSuite.tests.forEach(function (task) { task.options.plugins = task.options.plugins || []; task.options.plugins.push("estree"); const testFn = task.disabled ? it.skip : it; testFn(name + "/" + testSuite.title + "/" + task.title, function () { try { runTest(task, parseFunction, true); } catch (err) { const fixturePath = `${path.relative( rootPath, fixturesPath, )}/${name}/${task.actual.filename}`; throw new FixtureError(err, fixturePath, task.actual.code); } }); }); }); }); } // compact loc properties into a single line function compactFixture(jsonString) { return jsonString.replace( /"start": (\d+),\s+"end": (\d+),\s+"loc": \{\s+"start":\s\{\s+"line": (\d+),\s+"column": (\d+)\s+\},\s+"end":\s\{\s+"line": (\d+),\s+"column": (\d+)\s+\s+\}(?:,\s+"identifierName": "(\S+)")?\s+\}/gm, (_, p1, p2, p3, p4, p5, p6, p7) => { return ( `"start":${p1},"end":${p2},"loc":{"start":{"line":${p3},"column":${p4}},"end":{"line":${p5},"column":${p6}}` + (p7 ? `,"identifierName":"${p7}"}` : "}") ); }, ); } function save(test, ast) { fs.writeFileSync( test.expect.loc, compactFixture(JSON.stringify(ast, (k, v) => serialize(v), 2)), ); } /** * run parser on given tests * * @param {Test} A {@link packages/babel-helper-fixtures/src/index.js Test} instance generated from `getFixtures` * @param {*} parseFunction A parser with the same interface of `@babel/parser#parse` * @param {boolean} [compareErrorsOnly=false] Whether we should only compare the "errors" * of generated ast against the expected AST. Used for `runThrowTestsWithEstree` where an * ESTree AST is generated but we want to make sure `@babel/parser` still throws expected * recoverable errors on given code locations. * @returns {void} */ function runTest(test, parseFunction, compareErrorsOnly = false) { const opts = test.options; if (opts.throws && test.expect.code) { throw new Error( "File expected.json exists although options specify throws. Remove expected.json.", ); } let ast; try { ast = parseFunction(test.actual.code, { errorRecovery: true, ...opts }); } catch (err) { if (opts.throws) { if (err.message === opts.throws) { return; } else { if (process.env.OVERWRITE) { const fn = path.dirname(test.expect.loc) + "/options.json"; test.options = test.options || {}; test.options.throws = err.message; fs.writeFileSync(fn, JSON.stringify(test.options, null, 2)); return; } err.message = "Expected error message: " + opts.throws + ". Got error message: " + err.message; throw err; } } throw err; } if (ast.comments && !ast.comments.length) delete ast.comments; if (ast.errors && !ast.errors.length) delete ast.errors; if (!test.expect.code && !opts.throws && !process.env.CI) { test.expect.loc += "on"; return save(test, ast); } if (opts.throws) { if (process.env.OVERWRITE) { const fn = path.dirname(test.expect.loc) + "/options.json"; test.options = test.options || {}; delete test.options.throws; const contents = JSON.stringify(test.options, null, 2); if (contents === "{}") { fs.unlinkSync(fn); } else { fs.writeFileSync(fn, JSON.stringify(test.options, null, 2)); } test.expect.loc += "on"; return save(test, ast); } throw new Error( "Expected error message: " + opts.throws + ". But parsing succeeded.", ); } else if (compareErrorsOnly) { const mis = misMatch(JSON.parse(test.expect.code).errors, ast.errors); if (mis) { throw new Error(mis); } } else { const mis = misMatch(JSON.parse(test.expect.code), ast); if (mis) { if (process.env.OVERWRITE) { return save(test, ast); } throw new Error(mis); } } } function serialize(value) { if (typeof value === "bigint") { return { [serialized]: "BigInt", value: value.toString(), }; } else if (value instanceof RegExp) { return { [serialized]: "RegExp", source: value.source, flags: value.flags, }; } else if (value instanceof Error) { // Errors are serialized to a simple string, because are used frequently return value.toString(); } return value; } function ppJSON(v) { if (typeof v === "bigint" || v instanceof Error || v instanceof RegExp) { return ppJSON(serialize(v)); } if (v && typeof v === "object" && v[serialized]) { switch (v[serialized]) { case "BigInt": return typeof BigInt === "undefined" ? "null" : v.value + "n"; case "RegExp": return `/${v.source}/${v.flags}`; } } else if (typeof v === "string" && /^[A-Z][a-z]+Error: /.test(v)) { // Errors are serialized to a simple string, because are used frequently return v; } return JSON.stringify(v, (k, v) => serialize(v), 2); } function addPath(str, pt) { if (str.charAt(str.length - 1) === ")") { return str.slice(0, str.length - 1) + "/" + pt + ")"; } else { return str + " (" + pt + ")"; } } function misMatch(exp, act) { if ( act instanceof RegExp || act instanceof Error || typeof act === "bigint" || (exp && typeof exp === "object" && exp[serialized]) ) { const left = ppJSON(exp); const right = ppJSON(act); if (left !== right) return left + " !== " + right; } else if (Array.isArray(exp)) { if (!Array.isArray(act)) return ppJSON(exp) + " != " + ppJSON(act); if (act.length != exp.length) { return "array length mismatch " + exp.length + " != " + act.length; } for (let i = 0; i < act.length; ++i) { const mis = misMatch(exp[i], act[i]); if (mis) return addPath(mis, i); } } else if (!exp || !act || typeof exp != "object" || typeof act != "object") { if (exp !== act && typeof exp != "function") { return ppJSON(exp) + " !== " + ppJSON(act); } } else { for (const prop of Object.keys(exp)) { const mis = misMatch(exp[prop], act[prop]); if (mis) return addPath(mis, prop); } for (const prop of Object.keys(act)) { if (typeof act[prop] === "function") { continue; } if (!(prop in exp) && act[prop] !== undefined) { return `Did not expect a property '${prop}'`; } } } }