Add Doc[] as Doc (#9733)

Co-authored-by: Georgii Dolzhykov <thorn.mailbox@gmail.com>
This commit is contained in:
fisker CheungandGeorgii Dolzhykov authored and GitHub committed 2021-01-04 10:04:30 +08:00
1 parent 23521ded6c
commit 9d9c0e2b6f
22 files changed
+332 -93

No files matched your search

+4
View File
@@ -109,8 +109,12 @@ overrides:
prettier-internal-rules/better-parent-property-check-in-needs-parens: error
- files: src/**/*.js
rules:
prettier-internal-rules/no-doc-builder-concat: off
prettier-internal-rules/prefer-fast-path-each: error
prettier-internal-rules/prefer-is-non-empty-array: error
- files: src/document/*.js
rules:
prettier-internal-rules/no-doc-builder-concat: off
- files:
- src/language-*/**/*.js
rules:
+13
View File
@@ -0,0 +1,13 @@
#### Use arrays instead of `concat` (#9733 by @fisker, @thorn0)
To simplify the code of AST printers, the data structure for the concatenation command has been changed from `{ type: 'concat', parts: Doc[] }` to `Doc[]`. The old format is deprecated, but for compatibility, the doc printer still supports it, and `doc.builders.concat` (as well as some other builder functions) will keep using it until the next major version of Prettier.
If you're a plugin author, this change should only concern you if your plugin introspects or modifies composed docs. If it happens to be the case, please make your plugin compatible with future versions of Prettier by tweaking the introspecting code to support the new format. There also is an off-chance where this change can break things, namely if a plugin calls another plugin to [print an embedded language](https://prettier.io/docs/en/plugins.html#optional-embed) and then introspects the returned doc. There seems to be no reason for plugins to do that though.
```jsx
// Prettier stable
myDoc = group(concat(["foo", line, "bar"]));
// Prettier master
myDoc = group(["foo", line, "bar"]);
```
+18 -16
View File
@@ -2,14 +2,6 @@ The core of the algorithm is implemented in `doc-{printer,builders,utils,debug}.
The following commands are available:
### concat
```ts
declare function concat(docs: Doc[]): Doc;
```
Combine an array into a single string.
### group
```ts
@@ -97,7 +89,7 @@ declare var breakParent: Doc;
Include this anywhere to force all parent groups to break. See `group` for more info. Example:
```js
group(concat([" ", expr, " ", breakParent]));
group([" ", expr, " ", breakParent]);
```
### join
@@ -149,7 +141,7 @@ declare function lineSuffix(suffix: Doc): Doc;
This is used to implement trailing comments. In practice, it is not practical to find where the line ends and you don't want to accidentally print some code at the end of the comment. `lineSuffix` will buffer the output and flush it before any new line.
```js
concat(["a", lineSuffix(" // comment"), ";", hardline]);
["a", lineSuffix(" // comment"), ";", hardline];
```
will output
@@ -167,7 +159,7 @@ declare var lineSuffixBoundary: Doc;
In cases where you embed code inside of templates, comments shouldn't be able to leave the code part. lineSuffixBoundary is an explicit marker you can use to flush code in addition to newlines.
```js
concat(["{", lineSuffix(" // comment"), lineSuffixBoundary, "}", hardline]);
["{", lineSuffix(" // comment"), lineSuffixBoundary, "}", hardline];
```
will output
@@ -260,21 +252,31 @@ For an example, here's the implementation of the `ArrayExpression` node type:
<!-- prettier-ignore -->
```js
group(
concat([
[
"[",
indent(
concat([
[
line,
join(
concat([",", line]),
[",", line],
path.map(print, "elements")
)
])
]
),
line,
"]"
])
]
);
```
This is a group with opening and closing brackets, and possibly indented contents. Because it's a `group` it will always be broken up if any of the sub-expressions are broken.
### [Deprecated] concat
> This command is deprecated, use `Doc[]` instead.
```ts
declare function concat(docs: Doc[]): Doc;
```
Combine an array into a single string.
+1 -1
View File
@@ -170,7 +170,7 @@ export const printers = {
Prettier uses an intermediate representation, called a Doc, which Prettier then turns into a string (based on options like `printWidth`). A _printer_'s job is to take the AST generated by `parsers[<parser name>].parse` and return a Doc. A Doc is constructed using [builder commands](https://github.com/prettier/prettier/blob/master/commands.md):
```js
const { concat, join, line, ifBreak, group } = require("prettier").doc.builders;
const { join, line, ifBreak, group } = require("prettier").doc.builders;
```
The printing process works as follows:
+1 -1
View File
@@ -7,6 +7,6 @@ This printer is a fork of [recast](https://github.com/benjamn/recast)’s printe
The basic idea is that the printer takes an AST and returns an intermediate representation of the output, and the printer uses that to generate a string. The advantage is that the printer can "measure" the IR and see if the output is going to fit on a line, and break if not.
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `concat(["(", line, arg, line, ")"])` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/master/commands.md).
@@ -5,6 +5,7 @@ module.exports = {
"better-parent-property-check-in-needs-parens": require("./better-parent-property-check-in-needs-parens"),
"directly-loc-start-end": require("./directly-loc-start-end"),
"jsx-identifier-case": require("./jsx-identifier-case"),
"no-doc-builder-concat": require("./no-doc-builder-concat"),
"no-node-comments": require("./no-node-comments"),
"prefer-fast-path-each": require("./prefer-fast-path-each"),
"prefer-is-non-empty-array": require("./prefer-is-non-empty-array"),
@@ -0,0 +1,35 @@
"use strict";
const selector = [
"CallExpression",
">",
"Identifier.callee",
'[name="concat"]',
].join("");
const messageId = "no-doc-builder-concat";
module.exports = {
meta: {
type: "suggestion",
docs: {
url:
"https://github.com/prettier/prettier/blob/master/scripts/eslint-plugin-prettier-internal-rules/no-doc-builder-concat.js",
},
messages: {
[messageId]: "Use array directly instead of `concat([])`",
},
fixable: "code",
},
create(context) {
return {
[selector](node) {
context.report({
node,
messageId,
fix: (fixer) => fixer.replaceText(node, ""),
});
},
};
},
};
@@ -94,6 +94,22 @@ test("jsx-identifier-case", {
],
});
test("no-doc-builder-concat", {
valid: ["notConcat([])", "concat", "[].concat([])"],
invalid: [
{
code: "concat(parts)",
output: "(parts)",
errors: 1,
},
{
code: "concat(['foo', line])",
output: "(['foo', line])",
errors: 1,
},
],
});
test("no-node-comments", {
valid: [
"const comments = node.notComments",
+19 -8
View File
@@ -8,21 +8,32 @@
* @property {boolean} [hard]
* @property {boolean} [literal]
*
* @typedef {string | DocObject} Doc
* @typedef {Doc[]} DocArray
*
* @typedef {string | DocObject | DocArray} Doc
*/
/**
* @param {Doc} val
*/
function assertDoc(val) {
/* istanbul ignore if */
if (
!(typeof val === "string" || (val != null && typeof val.type === "string"))
) {
throw new Error(
"Value " + JSON.stringify(val) + " is not a valid document"
);
if (typeof val === "string") {
return;
}
if (Array.isArray(val)) {
for (const doc of val) {
assertDoc(doc);
}
return;
}
if (val && typeof val.type === "string") {
return;
}
/* istanbul ignore next */
throw new Error("Value " + JSON.stringify(val) + " is not a valid document");
}
/**
+13 -11
View File
@@ -1,12 +1,14 @@
"use strict";
function flattenDoc(doc) {
if (doc.type === "concat") {
const res = [];
const { isConcat, getDocParts } = require("./doc-utils");
for (let i = 0; i < doc.parts.length; ++i) {
const doc2 = doc.parts[i];
if (typeof doc2 !== "string" && doc2.type === "concat") {
function flattenDoc(doc) {
if (isConcat(doc)) {
const res = [];
const parts = getDocParts(doc);
for (let i = 0; i < parts.length; ++i) {
const doc2 = parts[i];
if (typeof doc2 !== "string" && isConcat(doc2)) {
res.push(...flattenDoc(doc2).parts);
} else {
const flattened = flattenDoc(doc2);
@@ -16,7 +18,7 @@ function flattenDoc(doc) {
}
}
return { ...doc, parts: res };
return { type: "concat", parts: res };
} else if (doc.type === "if-break") {
return {
...doc,
@@ -44,6 +46,10 @@ function printDoc(doc) {
return JSON.stringify(doc);
}
if (isConcat(doc)) {
return "[" + getDocParts(doc).map(printDoc).join(", ") + "]";
}
if (doc.type === "line") {
if (doc.literal) {
return "literalline";
@@ -65,10 +71,6 @@ function printDoc(doc) {
return "trim";
}
if (doc.type === "concat") {
return "[" + doc.parts.map(printDoc).join(", ") + "]";
}
if (doc.type === "indent") {
return "indent(" + printDoc(doc.contents) + ")";
}
+11 -12
View File
@@ -3,6 +3,7 @@
const { getStringWidth } = require("../common/util");
const { convertEndOfLineToChars } = require("../common/end-of-line");
const { concat, fill, cursor } = require("./doc-builders");
const { isConcat, getDocParts } = require("./doc-utils");
/** @type {Record<symbol, typeof MODE_BREAK | typeof MODE_FLAT>} */
let groupModeMap;
@@ -167,14 +168,13 @@ function fits(next, restCommands, width, options, mustBeFlat) {
out.push(doc);
width -= getStringWidth(doc);
} else if (isConcat(doc)) {
const parts = getDocParts(doc);
for (let i = parts.length - 1; i >= 0; i--) {
cmds.push([ind, mode, parts[i]]);
}
} else {
switch (doc.type) {
case "concat":
for (let i = doc.parts.length - 1; i >= 0; i--) {
cmds.push([ind, mode, doc.parts[i]]);
}
break;
case "indent":
cmds.push([makeIndent(ind, options), mode, doc.contents]);
@@ -264,17 +264,16 @@ function printDocToString(doc, options) {
const formatted = newLine !== "\n" ? doc.replace(/\n/g, newLine) : doc;
out.push(formatted);
pos += getStringWidth(formatted);
} else if (isConcat(doc)) {
const parts = getDocParts(doc);
for (let i = parts.length - 1; i >= 0; i--) {
cmds.push([ind, mode, parts[i]]);
}
} else {
switch (doc.type) {
case "cursor":
out.push(cursor.placeholder);
break;
case "concat":
for (let i = doc.parts.length - 1; i >= 0; i--) {
cmds.push([ind, mode, doc.parts[i]]);
}
break;
case "indent":
cmds.push([makeIndent(ind, options), mode, doc.contents]);
+35 -25
View File
@@ -2,6 +2,9 @@
const { literalline, concat } = require("./doc-builders");
const isConcat = (doc) => Array.isArray(doc) || (doc && doc.type === "concat");
const getDocParts = (doc) => (Array.isArray(doc) ? doc : doc.parts);
// Using a unique object to compare by reference.
const traverseDocOnExitStackMarker = {};
@@ -29,9 +32,10 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
// the parts need to be pushed onto the stack in reverse order,
// so that they are processed in the original order
// when the stack is popped.
if (doc.type === "concat" || doc.type === "fill") {
for (let ic = doc.parts.length, i = ic - 1; i >= 0; --i) {
docsStack.push(doc.parts[i]);
if (isConcat(doc) || doc.type === "fill") {
const parts = getDocParts(doc);
for (let ic = parts.length, i = ic - 1; i >= 0; --i) {
docsStack.push(parts[i]);
}
} else if (doc.type === "if-break") {
if (doc.flatContents) {
@@ -56,7 +60,9 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
}
function mapDoc(doc, cb) {
if (doc.type === "concat" || doc.type === "fill") {
if (Array.isArray(doc)) {
return cb(doc.map((part) => mapDoc(part, cb)));
} else if (doc.type === "concat" || doc.type === "fill") {
const parts = doc.parts.map((part) => mapDoc(part, cb));
return cb({ ...doc, parts });
} else if (doc.type === "if-break") {
@@ -191,23 +197,21 @@ function stripDocTrailingHardlineFromDoc(doc) {
return doc;
}
switch (doc.type) {
case "concat":
case "fill": {
const parts = [...doc.parts];
if (isConcat(doc) || doc.type === "fill") {
const parts = getDocParts(doc);
while (parts.length > 1 && isHardline(...parts.slice(-2))) {
parts.length -= 2;
}
if (parts.length > 0) {
const lastPart = stripDocTrailingHardlineFromDoc(
parts[parts.length - 1]
);
parts[parts.length - 1] = lastPart;
}
return { ...doc, parts };
while (parts.length > 1 && isHardline(...parts.slice(-2))) {
parts.length -= 2;
}
if (parts.length > 0) {
const lastPart = stripDocTrailingHardlineFromDoc(parts[parts.length - 1]);
parts[parts.length - 1] = lastPart;
}
return Array.isArray(doc) ? parts : { ...doc, parts };
}
switch (doc.type) {
case "align":
case "indent":
case "group":
@@ -230,7 +234,6 @@ function stripTrailingHardline(doc) {
return stripDocTrailingHardlineFromDoc(cleanDoc(doc));
}
const isConcat = (doc) => doc && doc.type === "concat";
function cleanDocFn(doc) {
switch (doc.type) {
case "fill":
@@ -271,11 +274,13 @@ function cleanDocFn(doc) {
}
const parts = [];
for (const part of doc.parts) {
for (const part of getDocParts(doc)) {
if (!part) {
continue;
}
const [currentPart, ...restParts] = isConcat(part) ? part.parts : [part];
const [currentPart, ...restParts] = isConcat(part)
? getDocParts(part)
: [part];
if (
typeof currentPart === "string" &&
typeof parts[parts.length - 1] === "string"
@@ -294,7 +299,7 @@ function cleanDocFn(doc) {
if (parts.length === 1) {
return parts[0];
}
return { ...doc, parts };
return Array.isArray(doc) ? parts : { ...doc, parts };
}
// A safer version of `normalizeDoc`
// - `normalizeDoc` concat strings and flat "concat" in `fill`, while `cleanDoc` don't
@@ -315,8 +320,8 @@ function normalizeParts(parts) {
continue;
}
if (part.type === "concat") {
restParts.unshift(...part.parts);
if (isConcat(part)) {
restParts.unshift(...getDocParts(part));
continue;
}
@@ -337,6 +342,9 @@ function normalizeParts(parts) {
function normalizeDoc(doc) {
return mapDoc(doc, (currentDoc) => {
if (Array.isArray(currentDoc)) {
return normalizeParts(currentDoc);
}
if (!currentDoc.parts) {
return currentDoc;
}
@@ -360,6 +368,8 @@ function replaceNewlinesWithLiterallines(doc) {
}
module.exports = {
isConcat,
getDocParts,
isEmpty,
willBreak,
isLineNext,
+4 -5
View File
@@ -22,7 +22,7 @@ const {
ifBreak,
breakParent,
},
utils: { removeLines },
utils: { removeLines, getDocParts },
} = require("../document");
const clean = require("./clean");
const embed = require("./embed");
@@ -102,7 +102,7 @@ function genericPrint(path, options, print) {
return concat([
nodes,
after ? ` ${after}` : "",
nodes.parts.length ? hardline : "",
getDocParts(nodes).length ? hardline : "",
]);
}
case "css-comment": {
@@ -892,9 +892,8 @@ function genericPrint(path, options, print) {
node.groups[2] &&
node.groups[2].type === "value-paren_group"
) {
printed.contents.contents.parts[1] = group(
printed.contents.contents.parts[1]
);
const parts = getDocParts(printed.contents.contents);
parts[1] = group(parts[1]);
return group(dedent(printed));
}
+2 -1
View File
@@ -2,6 +2,7 @@
const {
builders: { concat, group, hardline, ifBreak, indent, join, line, softline },
utils: { getDocParts },
} = require("../document");
const { locStart, locEnd } = require("./loc");
const clean = require("./clean");
@@ -141,7 +142,7 @@ function print(path, options, print) {
}
const value = path.call(print, "value");
const quotedValue = isText
? printStringLiteral(value.parts.join(), options)
? printStringLiteral(getDocParts(value).join(), options)
: value;
return concat([n.name, "=", quotedValue]);
}
+3 -3
View File
@@ -16,7 +16,7 @@ const {
softline,
concat,
},
utils: { mapDoc, cleanDoc },
utils: { mapDoc, cleanDoc, getDocParts },
} = require("../document");
const { replaceEndOfLineWith, isNonEmptyArray } = require("../common/util");
const { print: printFrontMatter } = require("../utils/front-matter");
@@ -378,7 +378,7 @@ function genericPrint(path, options, print) {
printClosingTagSuffix(node, options),
])
);
return typeof printed === "string" ? printed : fill(printed.parts);
return typeof printed === "string" ? printed : fill(getDocParts(printed));
}
case "docType":
return concat([
@@ -981,7 +981,7 @@ function getTextValueParts(node, value = node.value) {
dedentString(htmlTrimPreserveIndentation(value)),
hardline
)
: join(line, splitByHtmlWhitespace(value)).parts;
: getDocParts(join(line, splitByHtmlWhitespace(value)));
}
function printEmbeddedAttributeValue(node, originalTextToDoc, options) {
+4 -3
View File
@@ -4,7 +4,7 @@ const { printComments } = require("../../main/comments");
const { getLast } = require("../../common/util");
const {
builders: { concat, join, line, softline, group, indent, align, ifBreak },
utils: { cleanDoc },
utils: { cleanDoc, getDocParts },
} = require("../../document");
const {
hasLeadingOwnLineComment,
@@ -130,7 +130,8 @@ function printBinaryishExpression(path, options, print) {
const hasJsx = isJsxNode(n.right);
const firstGroupIndex = parts.findIndex(
(part) => typeof part !== "string" && part.type === "group"
(part) =>
typeof part !== "string" && !Array.isArray(part) && part.type === "group"
);
// Separate the leftmost expression, possibly with its leading comments.
@@ -278,7 +279,7 @@ function printBinaryishExpressions(
if (printed.type === "string") {
parts = [printed];
} else {
parts = printed.parts;
parts = getDocParts(printed);
}
}
} else {
+1
View File
@@ -102,6 +102,7 @@ function printObject(path, options, print) {
}, field);
});
/** @type {Doc[]} */
let separatorParts = [];
const props = propsAndLoc
.sort((a, b) => a.loc - b.loc)
+2 -2
View File
@@ -23,7 +23,7 @@ const {
indent,
group,
},
utils: { normalizeDoc },
utils: { normalizeDoc, getDocParts },
printer: { printDocToString },
} = require("../document");
const { replaceEndOfLineWith } = require("../common/util");
@@ -578,7 +578,7 @@ function printLine(path, value, options) {
}
function printTable(path, options, print) {
const hardlineWithoutBreakParent = hardline.parts[0];
const hardlineWithoutBreakParent = getDocParts(hardline)[0];
const node = path.getValue();
const columnMaxWidths = [];
+2 -1
View File
@@ -14,6 +14,7 @@ const {
literalline,
markAsRoot,
},
utils: { getDocParts },
} = require("../../document");
const {
getAncestorCount,
@@ -55,7 +56,7 @@ function printBlock(path, print, options) {
if (index === 0) {
contentsParts.push(hardline);
}
contentsParts.push(fill(join(line, lineWords).parts));
contentsParts.push(fill(getDocParts(join(line, lineWords))));
if (index !== lineContents.length - 1) {
contentsParts.push(
lineWords.length === 0 ? hardline : markAsRoot(literalline)
+2 -1
View File
@@ -14,6 +14,7 @@ const {
lineSuffix,
literalline,
},
utils: { getDocParts },
} = require("../document");
const { replaceEndOfLineWith, isPreviousLineEmpty } = require("../common/util");
const { insertPragma, isPragma } = require("./pragma");
@@ -444,7 +445,7 @@ function printFlowScalarContent(nodeType, content, options) {
return join(
hardline,
lineContents.map((lineContentWords) =>
fill(join(line, lineContentWords).parts)
fill(getDocParts(join(line, lineContentWords)))
)
);
}
+7 -3
View File
@@ -27,9 +27,13 @@ describe("API", () => {
});
const doc = printToDoc(code, options);
test("prettier.printToDoc", () => {
expect(doc.type).toBe("concat");
expect(Array.isArray(doc.parts)).toBe(true);
test("prettier.printToDoc", (done) => {
// If it's array, it's a `concat`
if (!Array.isArray(doc)) {
expect(doc.type).toBe("concat");
expect(Array.isArray(doc.parts)).toBe(true);
}
done();
});
const formatResultFromDoc = formatDoc(doc, options);
+138
View File
@@ -0,0 +1,138 @@
"use strict";
const prettier = require("prettier-local");
const {
concat,
join,
hardline,
literalline,
group,
fill,
lineSuffix,
breakParent,
ifBreak,
indent,
align,
} = prettier.doc.builders;
// TODO: Make these builders to use array if possible in 3.0.0
describe("doc builders", () => {
test.each([
["concat", concat(["1", "2"]), { type: "concat", parts: ["1", "2"] }],
[
"join",
join(concat(["1"]), ["2", concat(["3"])]),
{
type: "concat",
parts: [
"2",
{ type: "concat", parts: ["1"] },
{ type: "concat", parts: ["3"] },
],
},
],
[
"join(array)",
join(["1"], ["2", concat(["3"])]),
{ type: "concat", parts: ["2", ["1"], { type: "concat", parts: ["3"] }] },
],
[
"hardline",
hardline,
{ type: "concat", parts: [{ type: "line", hard: true }, breakParent] },
],
[
"literalline",
literalline,
{
type: "concat",
parts: [{ type: "line", hard: true, literal: true }, breakParent],
},
],
[
"group",
group(concat(["1"])),
{
type: "group",
id: undefined,
contents: { type: "concat", parts: ["1"] },
break: false,
expandedStates: undefined,
},
],
[
"group (array)",
group(["1"]),
{
type: "group",
id: undefined,
contents: ["1"],
break: false,
expandedStates: undefined,
},
],
["fill", fill(["1", "2"]), { type: "fill", parts: ["1", "2"] }],
[
"line-suffix",
lineSuffix(concat(["1"])),
{
type: "line-suffix",
contents: { type: "concat", parts: ["1"] },
},
],
[
"line-suffix(array)",
lineSuffix(["1"]),
{
type: "line-suffix",
contents: ["1"],
},
],
[
"if-break",
ifBreak(concat(["1"]), ["2"]),
{
type: "if-break",
breakContents: { type: "concat", parts: ["1"] },
flatContents: ["2"],
groupId: undefined,
},
],
[
"indent",
indent(concat(["1"])),
{
type: "indent",
contents: { type: "concat", parts: ["1"] },
},
],
[
"indent(array)",
indent(["1"]),
{
type: "indent",
contents: ["1"],
},
],
[
"align",
align(" ", concat(["1"])),
{
type: "align",
contents: { type: "concat", parts: ["1"] },
n: " ",
},
],
[
"align(array)",
align(" ", ["1"]),
{
type: "align",
contents: ["1"],
n: " ",
},
],
])("%s", (_, doc, expected) => {
expect(doc).toEqual(expected);
});
});