diff --git a/changelog_unreleased/api/12748.md b/changelog_unreleased/api/12748.md new file mode 100644 index 000000000..6f3943463 --- /dev/null +++ b/changelog_unreleased/api/12748.md @@ -0,0 +1,7 @@ +#### [HIGHLIGHT] Support plugin with async parse function (#12748 by @fisker) + +[`parse` function](https://prettier.io/docs/en/plugins.html#parsers) in plugin can return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) now. + + + +Note: Since [`embed` functions in plugin](https://prettier.io/docs/en/plugins.html#optional-embed) are still sync, plugins with async parser can't be used to format embed code. diff --git a/docs/plugins.md b/docs/plugins.md index 92bc40a29..ec8877bb6 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -121,7 +121,11 @@ export const parsers = { The signature of the `parse` function is: ```ts -function parse(text: string, parsers: object, options: object): AST; +function parse( + text: string, + parsers: object, + options: object +): Promise | AST; ``` The location extraction functions (`locStart` and `locEnd`) return the starting and ending locations of a given AST node: diff --git a/src/cli/format.js b/src/cli/format.js index c904f395e..95554d035 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -2,12 +2,14 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import chalk from "chalk"; import prettier from "../index.js"; -import { getStdin } from "../common/third-party.cjs"; +import thirdParty from "../common/third-party.cjs"; import { createIgnorer, errors } from "./prettier-internal.js"; import { expandPatterns, fixWindowsSlashes } from "./expand-patterns.js"; import getOptionsForFile from "./options/get-options-for-file.js"; import isTTY from "./is-tty.js"; +const { getStdin } = thirdParty; + let createTwoFilesPatch; async function diff(a, b) { if (!createTwoFilesPatch) { @@ -106,7 +108,7 @@ async function format(context, input, opt) { } if (context.argv.debugPrintDoc) { - const doc = prettier.__debug.printToDoc(input, opt); + const doc = await prettier.__debug.printToDoc(input, opt); return { formatted: (await prettier.__debug.formatDoc(doc)) + "\n" }; } @@ -122,7 +124,7 @@ async function format(context, input, opt) { } if (context.argv.debugPrintAst) { - const { ast } = prettier.__debug.parse(input, opt); + const { ast } = await prettier.__debug.parse(input, opt); return { formatted: JSON.stringify(ast), }; @@ -139,10 +141,10 @@ async function format(context, input, opt) { } else { const stringify = (obj) => JSON.stringify(obj, null, 2); const ast = stringify( - prettier.__debug.parse(input, opt, /* massage */ true).ast + (await prettier.__debug.parse(input, opt, /* massage */ true)).ast ); const past = stringify( - prettier.__debug.parse(pp, opt, /* massage */ true).ast + (await prettier.__debug.parse(pp, opt, /* massage */ true)).ast ); /* istanbul ignore next */ diff --git a/src/main/core.js b/src/main/core.js index 4820f6640..a3da2d9c1 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -32,12 +32,12 @@ function attachComments(text, ast, opts) { return astComments; } -function coreFormat(originalText, opts, addAlignmentSize = 0) { +async function coreFormat(originalText, opts, addAlignmentSize = 0) { if (!originalText || originalText.trim().length === 0) { return { formatted: "", cursorOffset: -1, comments: [] }; } - const { ast, text } = parse(originalText, opts); + const { ast, text } = await parse(originalText, opts); if (opts.cursorOffset >= 0) { const nodeResult = findNodeAtOffset(ast, opts.cursorOffset, opts); @@ -140,8 +140,8 @@ function coreFormat(originalText, opts, addAlignmentSize = 0) { }; } -function formatRange(originalText, opts) { - const { ast, text } = parse(originalText, opts); +async function formatRange(originalText, opts) { + const { ast, text } = await parse(originalText, opts); const { rangeStart, rangeEnd } = calculateRange(text, opts, ast); const rangeString = text.slice(rangeStart, rangeEnd); @@ -156,7 +156,7 @@ function formatRange(originalText, opts) { const alignmentSize = getAlignmentSize(indentString, opts.tabWidth); - const rangeResult = coreFormat( + const rangeResult = await coreFormat( rangeString, { ...opts, @@ -275,7 +275,6 @@ function hasPragma(text, options) { return !selectedParser.hasPragma || selectedParser.hasPragma(text); } -// eslint-disable-next-line require-await async function formatWithCursor(originalText, originalOptions) { let { hasBOM, text, options } = normalizeInputAndOptions( originalText, @@ -296,7 +295,7 @@ async function formatWithCursor(originalText, originalOptions) { let result; if (options.rangeStart > 0 || options.rangeEnd < text.length) { - result = formatRange(text, options); + result = await formatRange(text, options); } else { if ( !options.requirePragma && @@ -306,7 +305,7 @@ async function formatWithCursor(originalText, originalOptions) { ) { text = options.printer.insertPragma(text); } - result = coreFormat(text, options); + result = await coreFormat(text, options); } if (hasBOM) { @@ -323,12 +322,12 @@ async function formatWithCursor(originalText, originalOptions) { const prettier = { formatWithCursor, - parse(originalText, originalOptions, massage) { + async parse(originalText, originalOptions, massage) { const { text, options } = normalizeInputAndOptions( originalText, normalizeOptions(originalOptions) ); - const parsed = parse(text, options); + const parsed = await parse(text, options); if (massage) { parsed.ast = massageAST(parsed.ast, options); } @@ -352,9 +351,9 @@ const prettier = { return formatted; }, - printToDoc(originalText, options) { + async printToDoc(originalText, options) { options = normalizeOptions(options); - const { ast, text } = parse(originalText, options); + const { ast, text } = await parse(originalText, options); attachComments(text, ast, options); return printAstToDoc(ast, options); }, diff --git a/src/main/multiparser.js b/src/main/multiparser.js index 44eec89e5..58d0e4348 100644 --- a/src/main/multiparser.js +++ b/src/main/multiparser.js @@ -1,7 +1,7 @@ import { stripTrailingHardline } from "../document/utils.js"; import { normalize } from "./options.js"; import { ensureAllCommentsPrinted, attach } from "./comments.js"; -import { parse } from "./parser.js"; +import { parseSync } from "./parser.js"; function printSubtree(path, print, options, printAstToDoc) { if (options.printer.embed && options.embeddedLanguageFormatting === "auto") { @@ -39,8 +39,13 @@ function textToDoc( { passThrough: true } ); - const result = parse(text, nextOptions); + const result = parseSync(text, nextOptions); const { ast } = result; + + if (typeof ast?.then === "function") { + throw new TypeError("async parse is not supported in embed"); + } + text = result.text; const astComments = ast.comments; diff --git a/src/main/parser.js b/src/main/parser.js index 7953ee134..997d9c087 100644 --- a/src/main/parser.js +++ b/src/main/parser.js @@ -68,7 +68,7 @@ function requireParser(parser) { } } -function parse(text, opts) { +function callPluginParseFunction(originalText, opts) { const parsers = getParsers(opts); // Create a new object {parserName: parseFn}. Uses defineProperty() to only call @@ -91,27 +91,44 @@ function parse(text, opts) { const parser = resolveParser(opts, parsers); try { - if (parser.preprocess) { - text = parser.preprocess(text, opts); - } + const text = parser.preprocess + ? parser.preprocess(originalText, opts) + : originalText; + const result = parser.parse(text, parsersForCustomParserApi, opts); - return { - text, - ast: parser.parse(text, parsersForCustomParserApi, opts), - }; + return { text, result }; } catch (error) { - const { loc } = error; - - if (loc) { - const { codeFrameColumns } = require("@babel/code-frame"); - error.codeFrame = codeFrameColumns(text, loc, { highlightCode: true }); - error.message += "\n" + error.codeFrame; - throw error; - } - - /* istanbul ignore next */ - throw error.stack; + handleParseError(error, originalText); } } -export { parse, resolveParser }; +function handleParseError(error, text) { + const { loc } = error; + + if (loc) { + const { codeFrameColumns } = require("@babel/code-frame"); + error.codeFrame = codeFrameColumns(text, loc, { highlightCode: true }); + error.message += "\n" + error.codeFrame; + throw error; + } + + /* istanbul ignore next */ + throw error.stack; +} + +async function parse(originalText, opts) { + const { text, result } = callPluginParseFunction(originalText, opts); + + try { + return { text, ast: await result }; + } catch (error) { + handleParseError(error, originalText); + } +} + +function parseSync(originalText, opts) { + const { text, result } = callPluginParseFunction(originalText, opts); + return { text, ast: result }; +} + +export { parse, parseSync, resolveParser }; diff --git a/tests/config/format-test.js b/tests/config/format-test.js index 8925b4b60..94fea05b7 100644 --- a/tests/config/format-test.js +++ b/tests/config/format-test.js @@ -375,8 +375,8 @@ async function runTest({ // Some parsers skip parsing empty files if (formatResult.changed && code.trim()) { const { input, output } = formatResult; - const originalAst = parse(input, formatOptions); - const formattedAst = parse(output, formatOptions); + const originalAst = await parse(input, formatOptions); + const formattedAst = await parse(output, formatOptions); if (isAstUnstableTest) { expect(formattedAst).not.toEqual(originalAst); } else { @@ -431,8 +431,13 @@ function shouldSkipEolTest(code, options) { return false; } -function parse(source, options) { - return prettier.__debug.parse(source, options, /* massage */ true).ast; +async function parse(source, options) { + const { ast } = await prettier.__debug.parse( + source, + options, + /* massage */ true + ); + return ast; } const indexProperties = [ diff --git a/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs b/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs new file mode 100644 index 000000000..d6ce91c51 --- /dev/null +++ b/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs @@ -0,0 +1,34 @@ +"use strict"; + +const name = "async-parser"; + +module.exports = { + languages: [ + { + name, + parsers: [name], + }, + ], + parsers: { + [name]: { + parse: (text) => Promise.resolve({ text, isAsyncParserPluginAst: true }), + astFormat: name, + locStart() {}, + locEnd() {}, + }, + }, + printers: { + [name]: { + print(path) { + if (!path.getNode().isAsyncParserPluginAst) { + throw new Error("Unexpected parse result."); + } + + return "Formatted by async-parser plugin"; + }, + massageAstNode() { + return { text: "AST text value placeholder" }; + }, + }, + }, +}; diff --git a/tests/config/prettier-plugins/prettier-plugin-async-parser/package.json b/tests/config/prettier-plugins/prettier-plugin-async-parser/package.json new file mode 100644 index 000000000..a9a1ccd2b --- /dev/null +++ b/tests/config/prettier-plugins/prettier-plugin-async-parser/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "name": "prettier-plugin-async-parser", + "version": "1.0.0", + "main": "./index.cjs" +} diff --git a/tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..e2efbc9ed --- /dev/null +++ b/tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`async-parser.txt format 1`] = ` +====================================options===================================== +parsers: ["async-parser"] +printWidth: 80 + | printWidth +=====================================input====================================== +A text will format as "Formatted by async-parser plugin". + +=====================================output===================================== +Formatted by async-parser plugin +================================================================================ +`; diff --git a/tests/format/misc/plugins/async-parser/async-parser.txt b/tests/format/misc/plugins/async-parser/async-parser.txt new file mode 100644 index 000000000..951b30c58 --- /dev/null +++ b/tests/format/misc/plugins/async-parser/async-parser.txt @@ -0,0 +1 @@ +A text will format as "Formatted by async-parser plugin". diff --git a/tests/format/misc/plugins/async-parser/jsfmt.spec.js b/tests/format/misc/plugins/async-parser/jsfmt.spec.js new file mode 100644 index 000000000..a4d6b23b9 --- /dev/null +++ b/tests/format/misc/plugins/async-parser/jsfmt.spec.js @@ -0,0 +1,9 @@ +import createEsmUtils from "esm-utils"; + +const { require } = createEsmUtils(import.meta); + +const plugins = [ + require("../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs"), +]; + +run_spec(import.meta, ["async-parser"], { plugins }); diff --git a/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..151c0ea16 --- /dev/null +++ b/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`async-parser.txt format 1`] = ` +====================================options===================================== +parsers: ["async-parser"] +printWidth: 80 + | printWidth +=====================================input====================================== +A text will format as "Formatted by async-parser plugin". + +=====================================output===================================== +Formatted by async-parser plugin +================================================================================ +`; + +exports[`with-aync-parser-code-block.md format 1`] = ` +====================================options===================================== +parsers: ["markdown"] +printWidth: 80 + | printWidth +=====================================input====================================== +\`\`\`async-parser +Since we don't allow async parser in embed format, so this will not be changed. +\`\`\` + +\`\`\`uppercase-rocks +This text should be uppercased. +\`\`\` + +=====================================output===================================== +\`\`\`async-parser +Since we don't allow async parser in embed format, so this will not be changed. +\`\`\` + +\`\`\`uppercase-rocks +THIS TEXT SHOULD BE UPPERCASED. +\`\`\` + +================================================================================ +`; diff --git a/tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js b/tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js new file mode 100644 index 000000000..86c1e6b30 --- /dev/null +++ b/tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js @@ -0,0 +1,10 @@ +import createEsmUtils from "esm-utils"; + +const { require } = createEsmUtils(import.meta); + +const plugins = [ + require("../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs"), + require("../../../../config/prettier-plugins/prettier-plugin-uppercase-rocks/index.cjs"), +]; + +run_spec(import.meta, ["markdown"], { plugins }); diff --git a/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md b/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md new file mode 100644 index 000000000..66dd689df --- /dev/null +++ b/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md @@ -0,0 +1,7 @@ +```async-parser +Since we don't allow async parser in embed format, so this will not be changed. +``` + +```uppercase-rocks +This text should be uppercased. +``` diff --git a/tests/integration/__tests__/debug-api.js b/tests/integration/__tests__/debug-api.js index 2ca99d135..bf9c48fb6 100644 --- a/tests/integration/__tests__/debug-api.js +++ b/tests/integration/__tests__/debug-api.js @@ -16,17 +16,22 @@ const formatted = 'const foo = "bar";\n'; const options = { parser: "babel", originalText: code }; describe("API", () => { - const { ast } = parse(code, options); + let ast; + let doc; + beforeAll(async () => { + ({ ast } = await parse(code, options)); + doc = await printToDoc(code, options); + }); + test("prettier.parse", () => { expect(Array.isArray(ast.program.body)).toBe(true); }); - const { formatted: formatResultFromAST } = formatAST(ast, options); test("prettier.formatAST", () => { + const { formatted: formatResultFromAST } = formatAST(ast, options); expect(formatResultFromAST).toBe(formatted); }); - const doc = printToDoc(code, options); test("prettier.printToDoc", (done) => { // If it's array, it's a `concat` if (!Array.isArray(doc)) { @@ -36,8 +41,8 @@ describe("API", () => { done(); }); - const { formatted: stringFromDoc } = printDocToString(doc, options); test("prettier.printDocToString", () => { + const { formatted: stringFromDoc } = printDocToString(doc, options); expect(stringFromDoc).toBe(formatted); }); diff --git a/website/static/worker.js b/website/static/worker.js index 0988563c7..e9521a08e 100644 --- a/website/static/worker.js +++ b/website/static/worker.js @@ -91,7 +91,9 @@ async function handleMessage(message) { let ast; let errored = false; try { - ast = serializeAst(prettier.__debug.parse(message.code, options).ast); + ast = serializeAst( + (await prettier.__debug.parse(message.code, options)).ast + ); } catch (e) { errored = true; ast = String(e); @@ -110,7 +112,7 @@ async function handleMessage(message) { if (message.debug.doc) { try { response.debug.doc = await prettier.__debug.formatDoc( - prettier.__debug.printToDoc(message.code, options), + await prettier.__debug.printToDoc(message.code, options), { parser: "babel", plugins } ); } catch (e) {