Run AST compare test on verify parsers (#18712)
This commit is contained in:
1 parent
c025c7d5ab
commit
2e230f99e7
10 files changed
+185
-51
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Vendored
+8
@@ -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 });
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user