Run AST compare test on verify parsers (#18712)

This commit is contained in:
fisker Cheung authored and GitHub committed 2026-01-23 04:47:51 +08:00
1 parent c025c7d5ab
commit 2e230f99e7
10 files changed
+185 -51

No files matched your search

+7 -2
View File
@@ -159,14 +159,19 @@ jobs:
browser:
- "chrome"
- "firefox"
include:
# It's slow on firefox
- os: "ubuntu-latest"
browser: "chrome"
FULL_TEST: true
exclude:
# Hangs https://github.com/prettier/prettier/pull/18716
- os: "windows-latest"
browser: "chrome"
env:
FULL_TEST: ${{ matrix.os == 'ubuntu-latest' && true || '' }}
FULL_TEST: ${{ matrix.FULL_TEST }}
TEST_RUNTIME_BROWSER: ${{ matrix.browser }}
name: ${{ matrix.os == 'ubuntu-latest' && '[Full Test] ' || '' }} Browser(${{ matrix.browser }}) on ${{ startsWith(matrix.os, 'macos') && 'MacOS' || startsWith(matrix.os, 'windows') && 'Windows' || 'Linux' }}
name: ${{ matrix.FULL_TEST && '[Full Test] ' || '' }} Browser(${{ matrix.browser }}) on ${{ startsWith(matrix.os, 'macos') && 'MacOS' || startsWith(matrix.os, 'windows') && 'Windows' || 'Linux' }}
runs-on: ${{ matrix.os }}
needs: [build]
steps:
+58 -4
View File
@@ -1,10 +1,15 @@
import {
isArrayExpression,
isBigIntLiteral,
isMeaningfulEmptyStatement,
isNumericLiteral,
isStringLiteral,
} from "./utilities/index.js";
/**
@import {Node} from "./types/estree.js"
*/
const ignoredProperties = new Set([
"range",
"raw",
@@ -16,9 +21,11 @@ const ignoredProperties = new Set([
"start",
"end",
"loc",
"flags",
"errors",
"tokens",
// Hermes
"trailingComma",
"docblock",
]);
const removeTemplateElementsValue = (node) => {
@@ -38,18 +45,32 @@ function cleanKey(cloned, original, property) {
}
}
/**
@param {Node} original
@param {any} cloned
@param {Node | undefined} parent
*/
function clean(original, cloned, parent) {
if (original.type === "Program") {
delete cloned.sourceType;
}
if (
(original.type === "BigIntLiteral" || original.type === "Literal") &&
original.bigint
(isBigIntLiteral(original) ||
original.type === "BigIntLiteralTypeAnnotation") &&
"bigint" in original
) {
cloned.bigint = original.bigint.toLowerCase();
}
if (original.type === "RegExpLiteral") {
cloned.flags = [...original.flags].sort().join("");
}
if (original.type === "Literal" && "regex" in original) {
cloned.regex.flags = [...original.regex.flags].sort().join("");
}
// We remove extra `;` and add them when needed
if (
original.type === "EmptyStatement" &&
@@ -88,6 +109,7 @@ function clean(original, cloned, parent) {
original.type === "ImportAttribute" ||
original.type === "RecordDeclarationProperty" ||
original.type === "RecordDeclarationStaticProperty") &&
// @ts-expect-error -- safe
!original.computed
) {
cleanKey(cloned, original, "key");
@@ -101,6 +123,7 @@ function clean(original, cloned, parent) {
// styled-jsx
if (
original.type === "JSXElement" &&
original.openingElement.name.type === "JSXIdentifier" &&
original.openingElement.name.name === "style" &&
original.openingElement.attributes.some(
(attr) => attr.type === "JSXAttribute" && attr.name.name === "jsx",
@@ -129,7 +152,7 @@ function clean(original, cloned, parent) {
// We change quotes
if (
original.type === "JSXAttribute" &&
original.value?.type === "Literal" &&
isStringLiteral(original.value) &&
/["']|"|'/.test(original.value.value)
) {
cloned.value.value = original.value.value.replaceAll(
@@ -139,6 +162,7 @@ function clean(original, cloned, parent) {
}
// Angular Components: Inline HTML template and Inline CSS styles
// @ts-expect-error -- Fixme
const expression = original.expression || original.callee;
if (
original.type === "Decorator" &&
@@ -146,6 +170,7 @@ function clean(original, cloned, parent) {
expression.callee.name === "Component" &&
expression.arguments.length === 1
) {
// @ts-expect-error -- Fixme
const astProps = original.expression.arguments[0].properties;
for (const [
index,
@@ -182,12 +207,41 @@ function clean(original, cloned, parent) {
removeTemplateElementsValue(cloned.quasi);
}
if (
(original.type === "CallExpression" ||
original.type === "MemberExpression") &&
// @ts-expect-error -- safe
!original.optional
) {
delete cloned.optional;
}
// TODO: Only delete value when there is leading comment which is exactly
// `/* GraphQL */` or `/* HTML */`
// Also see ./embed.js
if (original.type === "TemplateLiteral") {
removeTemplateElementsValue(cloned);
}
// We don't add parentheses to `(a?.b)?.c`
if (original.type === "ChainExpression") {
return cloned.expression;
}
// https://github.com/babel/babel/issues/17719
if (
(original.type === "ClassDeclaration" ||
original.type === "ClassExpression") &&
!original.superTypeArguments &&
original.superClass?.type === "TSInstantiationExpression" &&
original.superClass.typeArguments
) {
const {
superClass: { typeArguments, expression },
} = cloned;
cloned.superTypeArguments = typeArguments;
cloned.superClass = expression;
}
}
clean.ignoredProperties = ignoredProperties;
+3 -1
View File
@@ -34,9 +34,11 @@ function shouldAddParenthesesToIdentifier(path) {
}
// `for ((let.a) of []);`
// `for ((let.a) in []);`
if (node.name === "let") {
const expression = path.findAncestor(
(node) => node.type === "ForOfStatement",
(node) =>
node.type === "ForOfStatement" || node.type === "ForInStatement",
)?.left;
if (
expression &&
+5 -2
View File
@@ -52,8 +52,10 @@ function postprocess(ast, options) {
delete program.interpreter;
}
if (isOxcTs && ast.hashbang) {
comments.unshift(ast.hashbang);
if (ast.hashbang) {
if (isOxcTs) {
comments.unshift(ast.hashbang);
}
delete ast.hashbang;
}
@@ -164,6 +166,7 @@ function postprocess(ast, options) {
case "ImportDeclaration":
if (parser === "hermes" && node.assertions && !node.attributes) {
node.attributes = node.assertions;
delete node.assertions;
}
break;
+6 -6
View File
@@ -10,17 +10,17 @@ function printLiteral(path, options /* , print*/) {
const { node } = path;
switch (node.type) {
case "RegExpLiteral": // Babel 6 Literal split
case "RegExpLiteral": // Babel
return printRegex(node);
case "BigIntLiteral":
case "BigIntLiteral": // Babel
return printBigInt(node.extra.raw);
case "NumericLiteral": // Babel 6 Literal split
case "NumericLiteral": // Babel
return printNumber(node.extra.raw);
case "StringLiteral": // Babel 6 Literal split
case "StringLiteral": // Babel
return replaceEndOfLine(printString(node.extra.raw, options));
case "NullLiteral": // Babel 6 Literal split
case "NullLiteral": // Babel
return "null";
case "BooleanLiteral": // Babel 6 Literal split
case "BooleanLiteral": // Babel
return String(node.value);
case "DirectiveLiteral":
return printDirective(node.extra.raw, options);
+8
View File
@@ -69,3 +69,11 @@ export type NumericLiteral =
export type StringLiteral =
| NodeMap["StringLiteral"]
| (NodeMap["Literal"] & { value: string });
export type RegExpLiteral =
| NodeMap["RegExpLiteral"]
| (NodeMap["Literal"] & { regexp: { pattern: string; flags: string } });
export type BigIntLiteral =
| NodeMap["BigIntLiteral"]
| (NodeMap["Literal"] & { bigint: string });
+34 -3
View File
@@ -38,7 +38,15 @@ import {
} from "./node-types.js";
/**
@import {Node, NodeMap, Comment, NumericLiteral, StringLiteral} from "../types/estree.js";
@import {
Node,
NodeMap,
Comment,
NumericLiteral,
StringLiteral,
RegExpLiteral,
BigIntLiteral,
} from "../types/estree.js";
@import AstPath from "../../common/ast-path.js";
*/
@@ -165,10 +173,29 @@ function isStringLiteral(node) {
);
}
/**
* @param {Node} node
* @returns {node is RegExpLiteral}
*/
function isRegExpLiteral(node) {
return (
node.type === "RegExpLiteral" ||
(node.type === "Literal" && Boolean(node.regex))
(node.type === "Literal" &&
// @ts-expect-error -- Safe
Boolean(node.regex))
);
}
/**
* @param {Node} node
* @returns {node is BigIntLiteral}
*/
function isBigIntLiteral(node) {
return (
node.type === "BigIntLiteral" ||
(node.type === "Literal" &&
// @ts-expect-error -- Safe
Boolean(node.bigint))
);
}
@@ -564,7 +591,10 @@ function isSimpleCallArgument(node, depth = 2) {
node = stripChainElementWrappers(node);
if (isRegExpLiteral(node)) {
return getStringWidth(node.pattern ?? node.regex.pattern) <= 5;
return (
// @ts-expect-error -- safe
getStringWidth(node.pattern ?? node.regex.pattern) <= 5
);
}
if (
@@ -1101,6 +1131,7 @@ export {
hasNodeIgnoreComment,
hasRestParameter,
isArrayExpression,
isBigIntLiteral,
isBinaryCastExpression,
isBinaryish,
isBitwiseOperator,
+21 -4
View File
@@ -1,3 +1,8 @@
const shouldSerializeAst = () =>
// `BigInt` can't be serialized in chrome
// `/(?ims:^[a-z])/u` can't be serialized in Node.js v20 (firefox)
true;
function responseInBrowser(function_, context) {
const { accessPath, optionsIndex, browser } = context;
@@ -7,8 +12,7 @@ function responseInBrowser(function_, context) {
value.comments = [];
}
// `BigInt` can't be serialized in chrome
if (accessPath === "__debug.parse" && browser === "chrome") {
if (accessPath === "__debug.parse" && shouldSerializeAst(context)) {
value.ast = serializeAst(value.ast);
}
@@ -46,8 +50,7 @@ function requestFromNode(function_, context) {
const { accessPath, optionsIndex, browser } = context;
function resolve({ value }) {
// `BigInt` can't be serialized in chrome
if (accessPath === "__debug.parse" && browser === "chrome") {
if (accessPath === "__debug.parse" && shouldSerializeAst(context)) {
value.ast = deserializeAst(value.ast);
}
@@ -140,6 +143,12 @@ function deserializeValue(value) {
return Number.POSITIVE_INFINITY;
case "bigint":
return BigInt(value.bigint);
case "regex":
try {
return new RegExp(value.pattern, value.flags);
} catch {
return null;
}
}
return value;
@@ -154,6 +163,14 @@ function serializeValue(value) {
return { [INTERNAL_VALUE_KIND]: "bigint", bigint: String(value) };
}
if (value instanceof RegExp) {
return {
[INTERNAL_VALUE_KIND]: "regex",
source: value.source,
flags: value.flags,
};
}
return value;
}
+42 -28
View File
@@ -73,8 +73,17 @@ const commentClosureTypecaseTests = new Set(
].map((directory) => path.join(__dirname, "../format/js", directory)),
);
const espreeDisabledTests = commentClosureTypecaseTests;
const acornDisabledTests = new Set();
const espreeDisabledTests = new Set([
...commentClosureTypecaseTests,
...["explicit-resource-management/valid-await-using-asi-assignment.js"].map(
(file) => path.join(__dirname, "../format/js", file),
),
]);
const acornDisabledTests = new Set(
["explicit-resource-management/valid-await-using-asi-assignment.js"].map(
(file) => path.join(__dirname, "../format/js", file),
),
);
const meriyahDisabledTests = new Set(
[
// Parsing to different ASTs
@@ -392,11 +401,10 @@ async function runTest({
let formatResult = mainParserFormatResult;
expect(formatResult).toBeDefined();
const isMainParser = mainParserFormatOptions.parser === parser;
// Verify parsers or error tests
if (
mainParserFormatResult.error ||
mainParserFormatOptions.parser !== parser
) {
if (mainParserFormatResult.error || !isMainParser) {
formatOptions = { ...mainParserFormatResult.options, parser };
const runFormat = () => format(code, formatOptions);
@@ -418,19 +426,38 @@ async function runTest({
// The result is assert to equals to `output`
if (typeof output === "string") {
expect(formatResult.eolVisualizedOutput).toBe(visualizeEndOfLine(output));
}
// All parsers have the same result, only snapshot the result from main parser
else {
expect(
createSnapshot(formatResult, {
parsers,
formatOptions,
CURSOR_PLACEHOLDER,
}),
).toMatchSnapshot();
}
if (!FULL_TEST) {
return;
}
// All parsers have the same result, only snapshot the result from main parser
expect(
createSnapshot(formatResult, {
parsers,
formatOptions,
CURSOR_PLACEHOLDER,
}),
).toMatchSnapshot();
// Some parsers skip parsing empty files
if (formatResult.changed && code.trim()) {
const { input, output } = formatResult;
const [originalAst, formattedAst] = await Promise.all(
[input, output].map((code) => parse(code, formatOptions)),
);
const isAstUnstableTest = isAstUnstable(filename, formatOptions);
if (!FULL_TEST) {
if (isAstUnstableTest) {
expect(formattedAst).not.toStrictEqual(originalAst);
} else {
expect(formattedAst).toStrictEqual(originalAst);
}
}
if (!isMainParser) {
return;
}
@@ -460,19 +487,6 @@ async function runTest({
}
}
const isAstUnstableTest = isAstUnstable(filename, formatOptions);
// Some parsers skip parsing empty files
if (formatResult.changed && code.trim()) {
const { input, output } = formatResult;
const originalAst = await parse(input, formatOptions);
const formattedAst = await parse(output, formatOptions);
if (isAstUnstableTest) {
expect(formattedAst).not.toStrictEqual(originalAst);
} else {
expect(formattedAst).toStrictEqual(originalAst);
}
}
if (!shouldSkipEolTest(code, formatResult.options)) {
for (const eol of ["\r\n", "\r"]) {
const { eolVisualizedOutput: output } = await format(
@@ -54,7 +54,7 @@ for ((let)[a] of foo);
for ((let)().a of foo);
for (letFoo of foo);
for (let.a in foo);
for ((let).a in foo);
for ((let)[a] in foo);
for (let of of let);