mirror of
https://github.com/hanzoai/babel.git
synced 2026-09-24 16:56:45 +00:00
244 lines
7.6 KiB
JavaScript
244 lines
7.6 KiB
JavaScript
import { multiple as getFixtures } from "@babel/helper-fixtures";
|
|
import checkDuplicateNodes from "@babel/helper-check-duplicate-nodes";
|
|
import { readFileSync, unlinkSync, writeFileSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import Difference from "./difference.js";
|
|
import FixtureError from "./fixture-error.js";
|
|
import toFuzzedOptions from "./to-fuzzed-options.js";
|
|
import { serialize, deserialize } from "./serialization.js";
|
|
import toContextualSyntaxError from "./to-contextual-syntax-error.js";
|
|
|
|
const { CI, OVERWRITE } = process.env;
|
|
const { stringify, parse: JSONParse } = JSON;
|
|
|
|
const writeFileWithNewline = (path, string) =>
|
|
writeFileSync(path, `${string}\n`, "utf-8");
|
|
|
|
/**
|
|
* run parser on given tests
|
|
*
|
|
* @param {string} fixturesPath A base search path for finding fixtures.
|
|
* @param {*} parseFunction A parser with the same interface of
|
|
* `@babel/parser#parse`
|
|
* @param {function} [filter] A filter function that takes a test and returns
|
|
* whether it should be run or not. Defaults to always returning true.
|
|
* @param {boolean} [onlyCompareErrors=false] Whether we should only compare the
|
|
* "errors" of generated AST against the expected AST. Used for tests where an
|
|
* ESTree AST is generated but we want to make sure `@babel/parser` still throws
|
|
* expected recoverable recoverable errors on given code locations.
|
|
* @param {boolean} [ignoreLocations=false] Whether we should ignore the
|
|
* "locations" of the AST nodes when comparing the generated AST against the
|
|
* expected AST.
|
|
* @returns {void}
|
|
*/
|
|
export default function runFixtureTests(
|
|
fixturesPath,
|
|
parseFunction,
|
|
filter = () => true,
|
|
onlyCompareErrors = false,
|
|
ignoreLocations = false,
|
|
) {
|
|
const fixtures = getFixtures(fixturesPath);
|
|
|
|
for (const [name, testSuites] of Object.entries(fixtures)) {
|
|
for (const { title, tests } of testSuites) {
|
|
for (const test of tests) {
|
|
if (!filter(test)) continue;
|
|
runAutogeneratedParseTests(
|
|
parseFunction,
|
|
`${name}/${title}`,
|
|
test,
|
|
onlyCompareErrors,
|
|
ignoreLocations,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function runAutogeneratedParseTests(
|
|
parse,
|
|
prefix,
|
|
task,
|
|
onlyCompareErrors = false,
|
|
) {
|
|
const { expect, options } = task;
|
|
const testFn = task.disabled ? it.skip : it;
|
|
|
|
const expected = deserialize(expect.loc, options, expect.code);
|
|
const title = `${prefix}/${task.title}`;
|
|
const toStartPosition = ({
|
|
startIndex = 0,
|
|
startLine = 1,
|
|
startColumn = 0,
|
|
}) => `(${startIndex}, ${startLine}, ${startColumn})`;
|
|
|
|
toFuzzedOptions(options)
|
|
.map(([adjust, options], index) => ({
|
|
...task,
|
|
title: `${title} start = ${toStartPosition(options)}`,
|
|
adjust,
|
|
options,
|
|
expected,
|
|
filename: task.actual.loc,
|
|
source: task.actual.code,
|
|
original: index === 0,
|
|
}))
|
|
.forEach(test =>
|
|
testFn(test.title, () => runParseTest(parse, test, onlyCompareErrors)),
|
|
);
|
|
}
|
|
|
|
const toJustErrors = result => ({
|
|
threw: result.threw,
|
|
ast: result.ast && { errors: result.ast.errors },
|
|
});
|
|
|
|
function runParseTest(parse, test, onlyCompareErrors) {
|
|
const { adjust, expected, source, filename, options } = test;
|
|
|
|
if (!OVERWRITE && expected.threw && expected.ast) {
|
|
throw Error(
|
|
"File expected.json exists although options specify throws. Remove expected.json.",
|
|
);
|
|
}
|
|
|
|
const actual = parseWithRecovery(parse, source, filename, options);
|
|
|
|
if (
|
|
Array.isArray(onlyCompareErrors) &&
|
|
actual.ast.errors &&
|
|
actual.ast.errors.some(error =>
|
|
onlyCompareErrors.includes(error.reasonCode),
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const difference = new Difference(
|
|
adjust,
|
|
onlyCompareErrors ? toJustErrors(expected) : expected,
|
|
onlyCompareErrors ? toJustErrors(actual) : actual,
|
|
);
|
|
|
|
// No differences means we passed and there's nothing left to do.
|
|
if (difference === Difference.None) return;
|
|
|
|
const testLocation = test.taskDir;
|
|
|
|
// We only write the output of the original test, not all it's auto-generated
|
|
// variations.
|
|
if (!test.original) {
|
|
const err = new Error();
|
|
err.message = `Test Failed: ${testLocation}\nFixtureError.fromDifference: ${
|
|
FixtureError.fromDifference(difference, actual).message
|
|
}`;
|
|
throw err;
|
|
}
|
|
|
|
// FIXME: We're just maintaining the legacy behavior of storing *just* the
|
|
// error `message` here, which differs from the error's `toString()` that we
|
|
// store for each error in the `errors` array. In both cases, we should
|
|
// serialize the full error to be able to property test locations,
|
|
// reasonCodes, etc.
|
|
const throws = actual.threw ? actual.threw.message : undefined;
|
|
const optionsLocation = join(testLocation, "options.json");
|
|
|
|
// We want to throw away the contents of `throws` here.
|
|
const { throws: expectedThrows, ...oldOptions } = readJSON(optionsLocation);
|
|
const newOptions = { ...oldOptions, ...(throws && { throws }) };
|
|
|
|
const normalLocation = join(testLocation, "output.json");
|
|
const extendedLocation = join(testLocation, "output.extended.json");
|
|
|
|
// If we're not overwriting the current values with whatever we get this time
|
|
// around, then we have a legitimate error that we need to report.
|
|
|
|
const shouldThrow =
|
|
expectedThrows !== undefined ||
|
|
existsSync(normalLocation) ||
|
|
existsSync(extendedLocation);
|
|
|
|
if (CI || (!OVERWRITE && shouldThrow)) {
|
|
const err = new Error();
|
|
err.message = `Test Failed: ${testLocation}\nFixtureError.fromDifference: ${
|
|
FixtureError.fromDifference(difference, actual).message
|
|
}`;
|
|
throw err;
|
|
}
|
|
|
|
// Store (or overwrite) the options file if there's anything to record,
|
|
// otherwise remove it.
|
|
if (Object.keys(newOptions).length <= 0) {
|
|
rmf(optionsLocation);
|
|
} else if (throws !== expectedThrows) {
|
|
// The idea here is that we shouldn't need to change anything if this doesn't
|
|
// throw, and stringify will produce different output than what prettier
|
|
// wants.
|
|
writeFileWithNewline(optionsLocation, stringify(newOptions, null, 2));
|
|
}
|
|
|
|
// When only comparing errors, we don't want to overwrite the AST JSON because
|
|
// it belongs to a different test.
|
|
if (onlyCompareErrors) return;
|
|
|
|
const [extended, serialized] = actual.ast ? serialize(actual.ast) : [];
|
|
const outputLocation =
|
|
serialized && (extended ? extendedLocation : normalLocation);
|
|
|
|
if (outputLocation) {
|
|
writeFileWithNewline(outputLocation, serialized);
|
|
}
|
|
|
|
// Remove any previous output files that are no longer valid, either because
|
|
// extension changed, or because we aren't writing it out at all anymore.
|
|
for (const location of [normalLocation, extendedLocation]) {
|
|
if (location !== outputLocation) {
|
|
rmf(location);
|
|
}
|
|
}
|
|
}
|
|
|
|
function readJSON(filename) {
|
|
try {
|
|
return JSONParse(readFileSync(filename, "utf-8"));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function rmf(path) {
|
|
try {
|
|
unlinkSync(path);
|
|
} catch (error) {
|
|
if (error.code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
|
|
function parseWithRecovery(parse, source, filename, options) {
|
|
try {
|
|
const ast = parse(source, { errorRecovery: true, ...options });
|
|
checkDuplicateNodes(ast);
|
|
|
|
// Normalize the AST
|
|
//
|
|
// TODO: We should consider doing something more involved here as
|
|
// we may miss bugs where we put unexpected falsy objects in these
|
|
// properties.
|
|
if (ast.comments && !ast.comments.length) delete ast.comments;
|
|
if (ast.errors && !ast.errors.length) delete ast.errors;
|
|
else {
|
|
ast.errors = ast.errors.map(error =>
|
|
toContextualSyntaxError(error, source, filename, options),
|
|
);
|
|
}
|
|
|
|
return { threw: false, ast };
|
|
} catch (error) {
|
|
return {
|
|
threw: toContextualSyntaxError(error, source, filename, options),
|
|
ast: false,
|
|
};
|
|
}
|
|
}
|