prettier-plugin-multiline-arrays
Version:
Prettier plugin to force all arrays to be multiline.
425 lines (424 loc) • 22.1 kB
JavaScript
import { getObjectTypedKeys, stringify } from '@augment-vir/common';
import { doc } from 'prettier';
import { isDocCommand } from '../augments/doc.js';
import { walkDoc } from './child-docs.js';
import { getCommentTriggers, parseNextLineCounts, } from './comment-triggers.js';
import { containsLeadingNewline } from './leading-new-line.js';
import { isArrayLikeNode } from './supported-node-types.js';
import { containsTrailingComma } from './trailing-comma.js';
const nestingSyntaxOpen = '[{(`';
const nestingSyntaxClose = ']})`';
const found = 'Found "[" but:';
function insertLinesIntoArray(inputDoc, manualWrap, lineCounts, wrapThreshold, debug) {
walkDoc(inputDoc, debug, (currentDoc, parentDocs, childIndex) => {
const currentParent = parentDocs[0];
const parentDoc = currentParent?.parent;
if (typeof currentDoc === 'string' && currentDoc.trim() === '[') {
const undoMutations = [];
let finalLineBreakExists = false;
function undoAllMutations() {
undoMutations.toReversed().forEach((undoMutation) => {
undoMutation();
});
}
if (!Array.isArray(parentDoc)) {
if (debug) {
console.error({ brokenParent: parentDoc, currentDoc });
}
throw new Error(`${found} parentDoc is not an array.`);
}
if (debug) {
console.info({ currentDoc, parentDoc });
console.info(stringify(parentDoc));
}
if (childIndex !== 0) {
/**
* This happens in some situations which we don't want to format in this plugin,
* like in type accessors:
*
* ```typescript
* type mockType = exampleObject['property'];
* ```
*/
return true;
}
const maybeBreak = parentDoc[childIndex + 2];
if (isDocCommand(maybeBreak) && maybeBreak.type === 'if-break') {
undoMutations.push(() => {
parentDoc[childIndex + 2] = maybeBreak;
});
parentDoc[childIndex + 2] = maybeBreak.breakContents;
}
const indentIndex = childIndex + 1;
const bracketSibling = parentDoc[indentIndex] === '' ? parentDoc[indentIndex + 1] : parentDoc[indentIndex];
if (debug) {
console.info({ bracketSibling });
}
if (bracketSibling === ']') {
return false;
}
if (!isDocCommand(bracketSibling) || bracketSibling.type !== 'indent') {
throw new Error(`${found} its sibling was not an indent Doc.: ${stringify(bracketSibling)}`);
}
const indentContents = bracketSibling.contents;
if (debug) {
console.info({ indentContents });
}
if (!Array.isArray(indentContents)) {
throw new TypeError(`${found} indent didn't have array contents.`);
}
if (indentContents.length < 2) {
if (debug) {
console.error(stringify(indentContents));
}
throw new Error(`${found} indent contents did not have at least 2 children`);
}
const startingLine = indentContents[0];
if (debug) {
console.info({ firstIndentContentsChild: startingLine });
}
if (!isDocCommand(startingLine) || startingLine.type !== 'line') {
if (Array.isArray(startingLine)) {
undoAllMutations();
return false;
}
else {
throw new TypeError(`${found} first indent child was not a line.`);
}
}
indentContents[0] = '';
undoMutations.push(() => {
indentContents[0] = startingLine;
});
const indentedContent = indentContents[1];
if (debug) {
console.info({
secondIndentContentsChild: indentedContent,
itsFirstChild: indentedContent[0],
});
}
if (!indentedContent) {
console.error('second indent child (indentedContent) is not defined:', {
indentContents,
indentedContent,
});
throw new Error(`${found} second indent child is not a fill.`);
}
if (!Array.isArray(indentedContent) &&
!(isDocCommand(indentedContent) && indentedContent.type === 'fill')) {
console.error('second indent child (indentCode) is not a fill doc or an array:', {
indentContents,
indentCode: indentedContent,
});
throw new Error(`${found} second indent child is not a fill doc or an array.`);
}
if (Array.isArray(indentedContent)
? indentedContent.length === 0
: indentedContent.parts.length === 0) {
throw new Error(`${found} indentedContent has no length.`);
}
// lineIndex is 0 indexed
let lineIndex = 0;
// columnCount is 1 indexed
let columnCount = 1;
if (debug) {
console.info(`>>>>>>>>>>>>>> Walking children for commas`);
}
let arrayChildCount = 0;
let forceFinalLineBreakExists = false;
if (!finalLineBreakExists) {
walkDoc(indentedContent, debug, (currentDoc, commaParents, commaChildIndex) => {
finalLineBreakExists = false;
const innerCurrentParent = commaParents[0];
const innerCurrentParentDoc = innerCurrentParent?.parent;
if (isDocCommand(currentDoc) && currentDoc.type === 'if-break') {
if (debug) {
console.info(`found final line break inside of if-break`);
}
finalLineBreakExists = true;
if (!innerCurrentParentDoc) {
throw new Error(`Found if-break without a parent`);
}
if (!Array.isArray(innerCurrentParentDoc)) {
throw new TypeError(`if-break parent is not an array`);
}
if (commaChildIndex == undefined) {
throw new Error(`if-break child index is undefined`);
}
innerCurrentParentDoc[commaChildIndex] = currentDoc.breakContents;
innerCurrentParentDoc.splice(commaChildIndex + 1, 0, doc.builders.breakParent);
undoMutations.push(() => {
innerCurrentParentDoc.splice(commaChildIndex + 1, 1);
innerCurrentParentDoc[commaChildIndex] = currentDoc;
});
}
else if (typeof currentDoc === 'string') {
if (!innerCurrentParentDoc) {
console.error({ innerParentDoc: innerCurrentParentDoc });
throw new Error(`Found string but innerParentDoc is not defined.`);
}
if (currentDoc && nestingSyntaxOpen.includes(currentDoc)) {
if (!Array.isArray(innerCurrentParentDoc)) {
throw new TypeError(`Found opening syntax but parent is not an array.`);
}
const closingIndex = innerCurrentParentDoc.findIndex((sibling) => typeof sibling === 'string' &&
sibling &&
nestingSyntaxClose.includes(sibling));
if (closingIndex < 0) {
throw new Error(`Could not find closing match for ${currentDoc}`);
}
// check that there's a line break before the ending of the array
if (innerCurrentParentDoc[closingIndex] !== ']') {
const closingSibling = innerCurrentParentDoc[closingIndex - 1];
if (debug) {
console.info({ closingIndex, closingSibling });
}
if (closingSibling &&
typeof closingSibling === 'object' &&
!Array.isArray(closingSibling) &&
closingSibling.type === 'line' &&
!closingSibling.soft) {
if (debug) {
console.info(`found final line break inside of closing sibling`);
}
finalLineBreakExists = true;
}
}
return false;
}
else if (currentDoc && nestingSyntaxClose.includes(currentDoc)) {
throw new Error(`Found closing syntax which shouldn't be walked`);
}
else if (currentDoc === ',') {
if (debug) {
console.info({ foundCommaIn: innerCurrentParentDoc });
}
if (!Array.isArray(innerCurrentParentDoc)) {
console.error({ innerParentDoc: innerCurrentParentDoc });
throw new Error(`Found comma but innerParentDoc is not an array.`);
}
if (commaChildIndex == undefined) {
throw new Error(`Found comma but childIndex is undefined.`);
}
let siblingIndex = commaChildIndex + 1;
let parentToMutate = innerCurrentParentDoc;
if (commaChildIndex === innerCurrentParentDoc.length - 1) {
const commaGrandParent = commaParents[1];
if (commaGrandParent == undefined) {
throw new Error(`Could not find grandparent of comma group.`);
}
if (commaGrandParent.childIndexInThisParent == undefined) {
throw new Error(`Could not find index of comma group parent`);
}
if (!Array.isArray(commaGrandParent.parent)) {
throw new TypeError(`Comma group grandparent is not an array.`);
}
siblingIndex = commaGrandParent.childIndexInThisParent + 1;
parentToMutate = commaGrandParent.parent;
}
if (debug) {
console.info({
commaParentDoc: innerCurrentParentDoc,
parentToMutate,
siblingIndex,
});
}
let maybeCommaSibling = parentToMutate[siblingIndex];
if (debug) {
console.info(`Trying to find comma sibling at index ${siblingIndex}`, stringify({ parentToMutate, maybeCommaSibling }));
}
function isCommaSibling(maybe) {
return ((isDocCommand(maybe) && maybe.type === 'line') ||
(Array.isArray(maybe) &&
isDocCommand(maybe[0]) &&
maybe[0].type === 'line'));
}
while (!isCommaSibling(maybeCommaSibling) &&
siblingIndex < parentToMutate.length) {
siblingIndex++;
maybeCommaSibling = parentToMutate[siblingIndex];
if (debug) {
console.info(`Trying to find comma sibling at index ${siblingIndex}`, parentToMutate, maybeCommaSibling);
}
}
if (debug) {
console.info(`Found comma sibling at index ${siblingIndex}`, parentToMutate, maybeCommaSibling);
}
if (!isCommaSibling(maybeCommaSibling)) {
throw new Error(`Found comma but its following sibling is not a line: ${stringify(maybeCommaSibling)}`);
}
const commaSibling = maybeCommaSibling;
const currentLineCountIndex = lineIndex % lineCounts.length;
const currentLineCount = lineCounts.length
? lineCounts[currentLineCountIndex]
: undefined;
arrayChildCount++;
if ((currentLineCount && columnCount === currentLineCount) ||
!currentLineCount) {
// if we're on the last element of the line then increment to the next line
lineIndex++;
columnCount = 1;
/**
* Don't use doc.builders.hardline here. It causes "invalid size
* error" which I don't understand and which has no other useful
* information or stack trace.
*/
if (debug) {
console.info({
breakingAfter: parentToMutate[siblingIndex - 1],
});
}
parentToMutate[siblingIndex] =
doc.builders.hardlineWithoutBreakParent;
}
else {
parentToMutate[siblingIndex] = ' ';
columnCount++;
}
undoMutations.push(() => {
parentToMutate[siblingIndex] = commaSibling;
});
}
}
else if (Array.isArray(currentDoc)) {
const firstPart = currentDoc[0];
const secondPart = currentDoc[1];
if (debug) {
console.info('got concat child doc');
console.info(currentDoc, { firstPart, secondPart });
console.info(isDocCommand(firstPart), isDocCommand(secondPart), firstPart?.type === 'line', firstPart?.hard, secondPart?.type === 'break-parent');
}
if (isDocCommand(firstPart) &&
isDocCommand(secondPart) &&
firstPart.type === 'line' &&
firstPart.hard &&
secondPart.type === 'break-parent') {
if (debug) {
console.info('concat child was indeed a line break');
}
forceFinalLineBreakExists = true;
return false;
}
else if (debug) {
console.info('concat child doc was not a line break');
}
}
return true;
});
}
if (forceFinalLineBreakExists) {
finalLineBreakExists = true;
}
if (!finalLineBreakExists) {
if (debug) {
console.info(`Parsed all array children but finalBreakHappened = ${finalLineBreakExists}`);
}
const closingBracketIndex = parentDoc.indexOf(']');
const preBracketChild = parentDoc[closingBracketIndex - 1];
if (isDocCommand(preBracketChild) && preBracketChild.type === 'line') {
parentDoc.splice(closingBracketIndex - 1, 1);
undoMutations.push(() => {
parentDoc.splice(closingBracketIndex - 1, 0, preBracketChild);
});
}
parentDoc.splice(closingBracketIndex - 1, 0, doc.builders.hardlineWithoutBreakParent);
undoMutations.push(() => {
parentDoc.splice(closingBracketIndex - 1, 1);
});
}
if (Array.isArray(indentedContent)) {
const oldIndentContentChild = indentContents[1];
indentContents.splice(1, 1, doc.builders.group([
doc.builders.hardlineWithoutBreakParent,
...indentedContent,
]));
undoMutations.push(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
indentContents[1] = oldIndentContentChild;
});
}
else {
const oldParts = indentedContent.parts;
indentedContent.parts = [
doc.builders.group([
doc.builders.hardlineWithoutBreakParent,
...indentedContent.parts,
]),
];
undoMutations.push(() => {
indentedContent.parts = oldParts;
});
}
if (arrayChildCount < wrapThreshold && !lineCounts.length && !manualWrap) {
undoAllMutations();
}
// don't walk any deeper
return false;
}
else if (debug) {
console.info({ ignoring: currentDoc });
}
return true;
});
if (debug) {
console.info('final doc:', stringify(inputDoc));
}
// return what is input because we perform mutations on it
return inputDoc;
}
function getLatestSetValue(currentLine, triggers) {
const relevantSetLineCountsKey = getObjectTypedKeys(triggers)
.sort()
.reduce((closestKey, currentKey) => {
if (Number(currentKey) < currentLine) {
const currentData = triggers[currentKey];
if (currentData && currentData.lineEnd > currentLine) {
return currentKey;
}
}
return closestKey;
}, '');
const relevantSetLineCount = triggers[relevantSetLineCountsKey]?.data;
return relevantSetLineCount;
}
export function printWithMultilineArrays(originalFormattedOutput, path, inputOptions, debug) {
const rootNode = path.stack[0];
if (!rootNode) {
throw new Error(`Could not find valid root node in ${path.stack.map((entry) => entry.type).join(',')}`);
}
const node = path.getNode();
if (node && isArrayLikeNode(node)) {
if (!node.loc) {
throw new Error(`Could not find location of node ${node.type}`);
}
const currentLineNumber = node.loc.start.line;
const lastLine = currentLineNumber - 1;
const commentTriggers = getCommentTriggers(rootNode, debug);
const originalText = inputOptions.originalText;
const splitOriginalText = originalText.split('\n');
const includesLeadingNewline = containsLeadingNewline(node.loc, node.elements, splitOriginalText, debug);
const includesTrailingComma = containsTrailingComma(node.loc, node.elements, splitOriginalText, debug);
const relevantSetLineCount = getLatestSetValue(currentLineNumber, commentTriggers.setLineCounts);
const lineCounts = commentTriggers.nextLineCounts[lastLine] ??
relevantSetLineCount ??
parseNextLineCounts(inputOptions.multilineArraysLinePattern, false, debug);
const relevantSetWrapCommentThreshold = getLatestSetValue(currentLineNumber, commentTriggers.setWrapThresholds);
const wrapThreshold = commentTriggers.nextWrapThresholds[lastLine] ??
relevantSetWrapCommentThreshold ??
(inputOptions.multilineArraysWrapThreshold < 0
? Infinity
: inputOptions.multilineArraysWrapThreshold);
const includesCommentTrigger = (commentTriggers.nextWrapThresholds[lastLine] ?? relevantSetWrapCommentThreshold) !=
undefined || !!lineCounts.length;
if (debug) {
console.info(`======= Starting call to ${insertLinesIntoArray.name}: =======`);
console.info({ options: { lineCounts, wrapThreshold } });
}
const manualWrap = includesCommentTrigger
? false
: includesTrailingComma || includesLeadingNewline;
const newDoc = insertLinesIntoArray(originalFormattedOutput, manualWrap, lineCounts, wrapThreshold, debug);
return newDoc;
}
return originalFormattedOutput;
}