diff --git a/.github/workflows/prod-test.yml b/.github/workflows/prod-test.yml index 4e64bffcc..643309013 100644 --- a/.github/workflows/prod-test.yml +++ b/.github/workflows/prod-test.yml @@ -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: diff --git a/src/language-js/clean.js b/src/language-js/clean.js index dbb88235f..419712b5c 100644 --- a/src/language-js/clean.js +++ b/src/language-js/clean.js @@ -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; diff --git a/src/language-js/parentheses/identifier.js b/src/language-js/parentheses/identifier.js index e417493b1..bff40895b 100644 --- a/src/language-js/parentheses/identifier.js +++ b/src/language-js/parentheses/identifier.js @@ -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 && diff --git a/src/language-js/parse/postprocess/index.js b/src/language-js/parse/postprocess/index.js index d3bd16532..1cb479f18 100644 --- a/src/language-js/parse/postprocess/index.js +++ b/src/language-js/parse/postprocess/index.js @@ -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; diff --git a/src/language-js/print/literal.js b/src/language-js/print/literal.js index 1c6dae145..aafeb7100 100644 --- a/src/language-js/print/literal.js +++ b/src/language-js/print/literal.js @@ -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); diff --git a/src/language-js/types/estree.d.ts b/src/language-js/types/estree.d.ts index 1fcd49fbe..dcbc9e1af 100644 --- a/src/language-js/types/estree.d.ts +++ b/src/language-js/types/estree.d.ts @@ -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 }); diff --git a/src/language-js/utilities/index.js b/src/language-js/utilities/index.js index d0d3d4579..6b85a95f3 100644 --- a/src/language-js/utilities/index.js +++ b/src/language-js/utilities/index.js @@ -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, diff --git a/tests/config/browser/utilities.js b/tests/config/browser/utilities.js index bad4a70b2..8ba71082f 100644 --- a/tests/config/browser/utilities.js +++ b/tests/config/browser/utilities.js @@ -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; } diff --git a/tests/config/run-format-test.js b/tests/config/run-format-test.js index 2641a9a86..16aa9e81b 100644 --- a/tests/config/run-format-test.js +++ b/tests/config/run-format-test.js @@ -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( diff --git a/tests/format/js/identifier/for-of/__snapshots__/format.test.js.snap b/tests/format/js/identifier/for-of/__snapshots__/format.test.js.snap index bf8d63d67..6f94785ef 100644 --- a/tests/format/js/identifier/for-of/__snapshots__/format.test.js.snap +++ b/tests/format/js/identifier/for-of/__snapshots__/format.test.js.snap @@ -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);