Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support async parser #12748

Merged
merged 25 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions 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.

<!-- TODO: Discuss this before v3 release -->

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.
6 changes: 5 additions & 1 deletion docs/plugins.md
Expand Up @@ -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> | AST;
```

The location extraction functions (`locStart` and `locEnd`) return the starting and ending locations of a given AST node:
Expand Down
12 changes: 7 additions & 5 deletions src/cli/format.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -116,7 +118,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" };
}

Expand All @@ -132,7 +134,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),
};
Expand All @@ -149,10 +151,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 */
Expand Down
23 changes: 11 additions & 12 deletions src/main/core.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand All @@ -156,7 +156,7 @@ function formatRange(originalText, opts) {

const alignmentSize = getAlignmentSize(indentString, opts.tabWidth);

const rangeResult = coreFormat(
const rangeResult = await coreFormat(
rangeString,
{
...opts,
Expand Down Expand Up @@ -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,
Expand All @@ -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 &&
Expand All @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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);
},
Expand Down
9 changes: 7 additions & 2 deletions 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") {
Expand Down Expand Up @@ -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;
Expand Down
53 changes: 35 additions & 18 deletions src/main/parser.js
Expand Up @@ -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
Expand All @@ -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;
handleParseError(error, originalText);
}
}

if (loc) {
const { codeFrameColumns } = require("@babel/code-frame");
error.codeFrame = codeFrameColumns(text, loc, { highlightCode: true });
error.message += "\n" + error.codeFrame;
throw error;
}
function handleParseError(error, text) {
const { loc } = error;

/* istanbul ignore next */
throw error.stack;
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);
}
}

export { parse, resolveParser };
function parseSync(originalText, opts) {
const { text, result } = callPluginParseFunction(originalText, opts);
return { text, ast: result };
}

export { parse, parseSync, resolveParser };
13 changes: 9 additions & 4 deletions tests/config/format-test.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = [
Expand Down
@@ -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" };
},
},
},
};
@@ -0,0 +1,6 @@
{
"private": true,
"name": "prettier-plugin-async-parser",
"version": "1.0.0",
"main": "./index.cjs"
}
@@ -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
================================================================================
`;
1 change: 1 addition & 0 deletions tests/format/misc/plugins/async-parser/async-parser.txt
@@ -0,0 +1 @@
A text will format as "Formatted by async-parser plugin".
9 changes: 9 additions & 0 deletions 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 });
@@ -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.
\`\`\`

================================================================================
`;
10 changes: 10 additions & 0 deletions 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 });