prettier-plugin-hug-call-arguments 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +219 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,8 +120,47 @@ db.updateTable("User").set({
|
|
|
120
120
|
## How it works
|
|
121
121
|
|
|
122
122
|
This plugin wraps Prettier's built-in `estree` printer and only overrides the
|
|
123
|
-
`print` step for `CallExpression` and
|
|
124
|
-
match one of a few narrow, specific
|
|
123
|
+
`print` step for `CallExpression`, `ArrayExpression`, and
|
|
124
|
+
`ArrowFunctionExpression` nodes that match one of a few narrow, specific
|
|
125
|
+
shapes.
|
|
126
|
+
|
|
127
|
+
**Preserving a layout you already chose:**
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// input
|
|
131
|
+
foo(
|
|
132
|
+
1,
|
|
133
|
+
2,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// with this plugin — kept exactly as written
|
|
137
|
+
foo(
|
|
138
|
+
1,
|
|
139
|
+
2,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// bar(1, 2, 3) stays on one line — nothing to preserve, so it's printed
|
|
143
|
+
// the normal, printWidth-aware way
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Prettier already does this for object literals by default (`objectWrap:
|
|
147
|
+
"preserve"`): write `{` with a newline before the first property and it
|
|
148
|
+
stays multi-line, even if it would fit on one line. This plugin extends that
|
|
149
|
+
same "respect how the author wrote it" behavior to call arguments, array
|
|
150
|
+
elements, and arrow function parameter lists, which Prettier doesn't
|
|
151
|
+
normally preserve — those always auto-collapse to fit `printWidth`
|
|
152
|
+
regardless of the original formatting.
|
|
153
|
+
|
|
154
|
+
- applies once there are at least two items with a newline right after the
|
|
155
|
+
opening `(`/`[` and before the first one — a single item has no "one per
|
|
156
|
+
line" layout to preserve, so that's left to this plugin's other hugging
|
|
157
|
+
behavior, or Prettier's own
|
|
158
|
+
- takes priority over every other feature below: a layout you explicitly
|
|
159
|
+
chose is never fought by this plugin's own hugging heuristics
|
|
160
|
+
- bails on comments anywhere in the list, a sparse array, TS type
|
|
161
|
+
arguments/parameters, or (for parameters) anything but a plain arrow
|
|
162
|
+
function — a `function` expression/declaration has a name and block body
|
|
163
|
+
to reproduce too, which is more than this narrow check takes on
|
|
125
164
|
|
|
126
165
|
**Hugging a call-wrapped callback:**
|
|
127
166
|
|
|
@@ -141,6 +180,28 @@ match one of a few narrow, specific shapes.
|
|
|
141
180
|
- there's no blank line between arguments, no comments on the relevant
|
|
142
181
|
nodes, and no TS type arguments
|
|
143
182
|
|
|
183
|
+
**Hugging a call whose sole argument needs it:**
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// input
|
|
187
|
+
firstValueFrom(client.getX({ fileKey: data.fileKey, frameCount: FRAME_COUNT }));
|
|
188
|
+
|
|
189
|
+
// with this plugin
|
|
190
|
+
firstValueFrom(client.getX({
|
|
191
|
+
fileKey: data.fileKey,
|
|
192
|
+
frameCount: FRAME_COUNT,
|
|
193
|
+
}));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- the call has exactly one argument, and it's directly an object or array
|
|
197
|
+
literal that needs multi-line printing
|
|
198
|
+
- Prettier already hugs this shape reliably *in isolation*, but its own
|
|
199
|
+
"does the opening line fit" check can still give up and break the call's
|
|
200
|
+
own parens too once this plugin has fused several outer layers together
|
|
201
|
+
(pushing the real column deeper than Prettier expected) — there's
|
|
202
|
+
essentially never a readability win to that fallback over just hugging
|
|
203
|
+
directly, so this always does instead
|
|
204
|
+
|
|
144
205
|
**Hugging an arrow function's concise body:**
|
|
145
206
|
|
|
146
207
|
```js
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ const doc_1 = require("prettier/doc");
|
|
|
59
59
|
// what `require`/interop resolves to, and its printer object is what we
|
|
60
60
|
// spread and delegate to for every node we don't special-case.
|
|
61
61
|
const estree = __importStar(require("prettier/plugins/estree"));
|
|
62
|
-
const { group, indent, line, softline, join, ifBreak, breakParent } = doc_1.builders;
|
|
62
|
+
const { group, indent, line, softline, hardline, join, ifBreak, breakParent, } = doc_1.builders;
|
|
63
63
|
const { willBreak } = doc_1.utils;
|
|
64
64
|
// `estree.printers.estree` is typed as `Printer<any>` by Prettier, which is
|
|
65
65
|
// assignable to `Printer<ESNode>` on its own — no cast needed.
|
|
@@ -67,7 +67,15 @@ const estreePrinter = estree.printers.estree;
|
|
|
67
67
|
function hasComment(node) {
|
|
68
68
|
return Boolean(node?.comments && node.comments.length > 0);
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The terminal thing worth hugging at the bottom of a chain of wrapping
|
|
72
|
+
* calls: a function (the common `catchAsync(cb)` case), or a plain
|
|
73
|
+
* object/array literal directly (e.g. `firstValueFrom(client.getX({ ... }))`
|
|
74
|
+
* — the object isn't itself wrapped in a function, but it's exactly what
|
|
75
|
+
* Prettier would hug directly if it were the argument of a plain, unwrapped
|
|
76
|
+
* call).
|
|
77
|
+
*/
|
|
78
|
+
function isHuggableLiteral(node) {
|
|
71
79
|
if (!node) {
|
|
72
80
|
return false;
|
|
73
81
|
}
|
|
@@ -80,7 +88,7 @@ function isHuggableFunction(node) {
|
|
|
80
88
|
body?.type === "ObjectExpression" ||
|
|
81
89
|
body?.type === "ArrayExpression");
|
|
82
90
|
}
|
|
83
|
-
return
|
|
91
|
+
return node.type === "ObjectExpression" || node.type === "ArrayExpression";
|
|
84
92
|
}
|
|
85
93
|
/**
|
|
86
94
|
* A short, unbreakable-looking companion argument — the kind lodash-style
|
|
@@ -118,7 +126,7 @@ function couldExpandArg(node, depth = 0) {
|
|
|
118
126
|
if (!node || depth > 4) {
|
|
119
127
|
return false;
|
|
120
128
|
}
|
|
121
|
-
if (
|
|
129
|
+
if (isHuggableLiteral(node)) {
|
|
122
130
|
return true;
|
|
123
131
|
}
|
|
124
132
|
if (node.type === "CallExpression" &&
|
|
@@ -194,6 +202,35 @@ function wouldBreakStandalone(doc, options) {
|
|
|
194
202
|
}
|
|
195
203
|
return doc_1.printer.printDocToString(doc, options).formatted.includes("\n");
|
|
196
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Prettier's own doc printer doesn't reliably re-check `printWidth` for
|
|
207
|
+
* plain content that comes *after* a forced break inside a conditionalGroup
|
|
208
|
+
* alternative — this is the same class of limitation their own
|
|
209
|
+
* `isHopefullyShortCallArgument` hack works around (see
|
|
210
|
+
* https://github.com/prettier/prettier/issues/2456). So rather than trust
|
|
211
|
+
* `conditionalGroup` to reject an overflowing candidate on its own, render it
|
|
212
|
+
* standalone and check every line ourselves.
|
|
213
|
+
*/
|
|
214
|
+
/**
|
|
215
|
+
* Prettier's own member/call-chain printer makes its "should I break at the
|
|
216
|
+
* dots" decision using context (the surrounding call, the real column) this
|
|
217
|
+
* plugin's manually-reconstructed docs don't provide when they print a
|
|
218
|
+
* callee in isolation via `print("callee")` — which can make a callee that
|
|
219
|
+
* vanilla Prettier would never break on its own (see the linked issue)
|
|
220
|
+
* break at its dots anyway once disconnected from that context. Rendering
|
|
221
|
+
* it standalone at an effectively unlimited width and using the resulting
|
|
222
|
+
* plain text instead sidesteps the whole problem: nothing left to make that
|
|
223
|
+
* decision differently. If something still forces a real break even at
|
|
224
|
+
* unlimited width (a comment, most likely), this returns `undefined` and
|
|
225
|
+
* the caller should bail rather than risk it.
|
|
226
|
+
*/
|
|
227
|
+
function renderFlatOrUndefined(doc, options) {
|
|
228
|
+
const { formatted } = doc_1.printer.printDocToString(doc, {
|
|
229
|
+
...options,
|
|
230
|
+
printWidth: Number.MAX_SAFE_INTEGER,
|
|
231
|
+
});
|
|
232
|
+
return formatted.includes("\n") ? undefined : formatted;
|
|
233
|
+
}
|
|
197
234
|
/**
|
|
198
235
|
* Prettier's own doc printer doesn't reliably re-check `printWidth` for
|
|
199
236
|
* plain content that comes *after* a forced break inside a conditionalGroup
|
|
@@ -209,6 +246,172 @@ function fitsWhenFlattened(doc, options) {
|
|
|
209
246
|
.split("\n")
|
|
210
247
|
.every((line) => line.length <= options.printWidth);
|
|
211
248
|
}
|
|
249
|
+
function getTrailingComma(options, kind) {
|
|
250
|
+
if (options.trailingComma === "all") {
|
|
251
|
+
return ",";
|
|
252
|
+
}
|
|
253
|
+
if (options.trailingComma === "es5" && kind === "array") {
|
|
254
|
+
return ",";
|
|
255
|
+
}
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Prettier already preserves this for object literals by default
|
|
260
|
+
* (`objectWrap: "preserve"`): write `{` with a newline before the first
|
|
261
|
+
* property and it stays multi-line, even if it would otherwise fit on one
|
|
262
|
+
* line. Prettier doesn't extend that courtesy to call arguments, array
|
|
263
|
+
* elements, or parameter lists — those always auto-collapse to fit
|
|
264
|
+
* `printWidth` regardless of how they were originally written. This
|
|
265
|
+
* reproduces the same "respect how the author wrote it" behavior for those
|
|
266
|
+
* three, so the plugin's own hugging heuristics never fight a layout the
|
|
267
|
+
* developer explicitly chose by hand.
|
|
268
|
+
*
|
|
269
|
+
* Only applies once there are at least two items — with a single item,
|
|
270
|
+
* there's no "one per line" layout to preserve, and that case is already
|
|
271
|
+
* handled by this plugin's other hugging behavior (or Prettier's own).
|
|
272
|
+
* Bails on anything that would make correctly locating the opening bracket
|
|
273
|
+
* or reproducing the layout unsafe: comments anywhere in the list, a sparse
|
|
274
|
+
* array, or (for calls) TS type arguments.
|
|
275
|
+
*/
|
|
276
|
+
function tryPreserveMultilineList(path, options, print, kind) {
|
|
277
|
+
const { node } = path;
|
|
278
|
+
let items;
|
|
279
|
+
let openChar;
|
|
280
|
+
let closeChar;
|
|
281
|
+
let searchFrom;
|
|
282
|
+
let prefixDoc = "";
|
|
283
|
+
let suffixDoc = "";
|
|
284
|
+
let printItems;
|
|
285
|
+
if (kind === "call") {
|
|
286
|
+
if (node.type !== "CallExpression" ||
|
|
287
|
+
node.optional ||
|
|
288
|
+
node.typeArguments ||
|
|
289
|
+
node.typeParameters ||
|
|
290
|
+
!node.callee ||
|
|
291
|
+
!isSimpleCallee(node.callee) ||
|
|
292
|
+
// Let chain-flattening handle the outermost call of a chain instead —
|
|
293
|
+
// this only steps in for a plain, non-chained call.
|
|
294
|
+
(node.callee.type === "MemberExpression" &&
|
|
295
|
+
node.callee.object?.type === "CallExpression")) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
items = node.arguments ?? [];
|
|
299
|
+
openChar = "(";
|
|
300
|
+
closeChar = ")";
|
|
301
|
+
searchFrom = node.callee.end;
|
|
302
|
+
prefixDoc = print("callee");
|
|
303
|
+
printItems = () => path.map(print, "arguments");
|
|
304
|
+
}
|
|
305
|
+
else if (kind === "array") {
|
|
306
|
+
if (node.type !== "ArrayExpression") {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
items = node.elements ?? [];
|
|
310
|
+
openChar = "[";
|
|
311
|
+
closeChar = "]";
|
|
312
|
+
searchFrom = node.start;
|
|
313
|
+
printItems = () => path.map(print, "elements");
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Arrow functions only — a plain `function` expression/declaration also
|
|
317
|
+
// has a name and a block body to reproduce, which is more than this
|
|
318
|
+
// narrow check is worth taking on.
|
|
319
|
+
if (node.type !== "ArrowFunctionExpression" ||
|
|
320
|
+
node.typeParameters ||
|
|
321
|
+
node.returnType ||
|
|
322
|
+
node.predicate ||
|
|
323
|
+
!node.body ||
|
|
324
|
+
hasComment(node.body)) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
items = node.params ?? [];
|
|
328
|
+
openChar = "(";
|
|
329
|
+
closeChar = ")";
|
|
330
|
+
searchFrom = node.start;
|
|
331
|
+
prefixDoc = node.async ? "async " : "";
|
|
332
|
+
suffixDoc = [" => ", print("body")];
|
|
333
|
+
printItems = () => path.map(print, "params");
|
|
334
|
+
}
|
|
335
|
+
if (items.length < 2 ||
|
|
336
|
+
hasComment(node) ||
|
|
337
|
+
items.some((item) => !item || hasComment(item)) ||
|
|
338
|
+
searchFrom === undefined) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const openIndex = options.originalText.indexOf(openChar, searchFrom);
|
|
342
|
+
const firstStart = items[0]?.start;
|
|
343
|
+
if (openIndex === -1 || firstStart === undefined) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
const between = options.originalText.slice(openIndex + 1, firstStart);
|
|
347
|
+
if (!/\n/.test(between)) {
|
|
348
|
+
// Not originally multi-line — nothing to preserve; let this plugin's
|
|
349
|
+
// other hugging attempts, or Prettier's own default, decide.
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
if (kind === "call") {
|
|
353
|
+
const flatPrefix = renderFlatOrUndefined(prefixDoc, options);
|
|
354
|
+
if (flatPrefix === undefined) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
prefixDoc = flatPrefix;
|
|
358
|
+
}
|
|
359
|
+
const printedItems = printItems();
|
|
360
|
+
const listDoc = [
|
|
361
|
+
openChar,
|
|
362
|
+
indent([
|
|
363
|
+
hardline,
|
|
364
|
+
join([",", hardline], printedItems),
|
|
365
|
+
getTrailingComma(options, kind),
|
|
366
|
+
]),
|
|
367
|
+
hardline,
|
|
368
|
+
closeChar,
|
|
369
|
+
];
|
|
370
|
+
return [breakParent, prefixDoc, listDoc, suffixDoc];
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* A call with exactly one argument that's directly an object or array
|
|
374
|
+
* literal — `getX({ ... })` — is already hugged reliably by Prettier on its
|
|
375
|
+
* own, *in isolation*. But Prettier's own "hug the sole argument" mechanism
|
|
376
|
+
* is still gated on whether the opening line fits at the real column, and
|
|
377
|
+
* gives up (breaking the call's own parens too) when it doesn't — which
|
|
378
|
+
* becomes very possible once this plugin has already fused several outer
|
|
379
|
+
* layers together (e.g. `firstValueFrom(client.getX({ ... }))`, once
|
|
380
|
+
* `firstValueFrom(...)` hugs, pushes `getX(`'s real column much deeper).
|
|
381
|
+
* There's essentially never a readability win to giving up like that
|
|
382
|
+
* instead of just hugging directly, so this always does — but, like the
|
|
383
|
+
* plugin's other hugging features, only once the argument actually needs
|
|
384
|
+
* multi-line printing; a short object/array that already fits is left
|
|
385
|
+
* alone.
|
|
386
|
+
*/
|
|
387
|
+
function tryHugSoleLiteralArg(path, options, print) {
|
|
388
|
+
const { node } = path;
|
|
389
|
+
if (node.type !== "CallExpression" ||
|
|
390
|
+
node.optional ||
|
|
391
|
+
node.typeArguments ||
|
|
392
|
+
node.typeParameters ||
|
|
393
|
+
!node.callee ||
|
|
394
|
+
!isSimpleCallee(node.callee)) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
const args = node.arguments ?? [];
|
|
398
|
+
const [only] = args;
|
|
399
|
+
if (args.length !== 1 ||
|
|
400
|
+
!only ||
|
|
401
|
+
hasComment(only) ||
|
|
402
|
+
(only.type !== "ObjectExpression" && only.type !== "ArrayExpression")) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
const argDoc = path.map(print, "arguments")[0];
|
|
406
|
+
if (argDoc === undefined || !wouldBreakStandalone(argDoc, options)) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
const calleeDoc = renderFlatOrUndefined(print("callee"), options);
|
|
410
|
+
if (calleeDoc === undefined) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
return [breakParent, calleeDoc, "(", argDoc, ")"];
|
|
414
|
+
}
|
|
212
415
|
function tryHugWrappedCallback(path, options, print) {
|
|
213
416
|
const { node } = path;
|
|
214
417
|
if (node.type !== "CallExpression" || node.optional) {
|
|
@@ -244,12 +447,16 @@ function tryHugWrappedCallback(path, options, print) {
|
|
|
244
447
|
!wouldBreakStandalone(expandDoc, options)) {
|
|
245
448
|
return undefined;
|
|
246
449
|
}
|
|
450
|
+
const calleeDoc = renderFlatOrUndefined(print("callee"), options);
|
|
451
|
+
if (calleeDoc === undefined) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
247
454
|
const beforeDocs = printedArgs.slice(0, expandIndex);
|
|
248
455
|
const afterDocs = printedArgs.slice(expandIndex + 1);
|
|
249
456
|
const beforeWithCommas = beforeDocs.flatMap((doc) => [doc, ", "]);
|
|
250
457
|
const afterWithCommas = afterDocs.flatMap((doc) => [", ", doc]);
|
|
251
458
|
const primaryDoc = [
|
|
252
|
-
|
|
459
|
+
calleeDoc,
|
|
253
460
|
"(",
|
|
254
461
|
...beforeWithCommas,
|
|
255
462
|
group(expandDoc, { shouldBreak: true }),
|
|
@@ -458,7 +665,13 @@ const plugin = {
|
|
|
458
665
|
estree: {
|
|
459
666
|
...estreePrinter,
|
|
460
667
|
print(path, options, print, args) {
|
|
461
|
-
|
|
668
|
+
// Preserving a layout the developer explicitly chose by hand takes
|
|
669
|
+
// priority over this plugin's own hugging heuristics.
|
|
670
|
+
const hugged = tryPreserveMultilineList(path, options, print, "call") ??
|
|
671
|
+
tryPreserveMultilineList(path, options, print, "array") ??
|
|
672
|
+
tryPreserveMultilineList(path, options, print, "params") ??
|
|
673
|
+
tryHugWrappedCallback(path, options, print) ??
|
|
674
|
+
tryHugSoleLiteralArg(path, options, print) ??
|
|
462
675
|
tryHugArrowBody(path, options, print) ??
|
|
463
676
|
tryHugChainArgument(path, options, print);
|
|
464
677
|
return hugged === undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prettier-plugin-hug-call-arguments",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Prettier plugin: hug the last call argument even when it's wrapped in another call, e.g. app.delete(\"/x\", catchAsync(async (req, res) => { ... })). Fixes https://github.com/prettier/prettier/issues/11080",
|
|
5
5
|
"author": "soso tsertsvadze",
|
|
6
6
|
"license": "MIT",
|