prettier-plugin-hug-call-arguments 0.1.9 → 0.1.11

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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +96 -4
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -180,6 +180,28 @@ regardless of the original formatting.
180
180
  - there's no blank line between arguments, no comments on the relevant
181
181
  nodes, and no TS type arguments
182
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
+
183
205
  **Hugging an arrow function's concise body:**
184
206
 
185
207
  ```js
package/dist/index.js CHANGED
@@ -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
- function isHuggableFunction(node) {
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 false;
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 (isHuggableFunction(node)) {
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
@@ -312,6 +349,13 @@ function tryPreserveMultilineList(path, options, print, kind) {
312
349
  // other hugging attempts, or Prettier's own default, decide.
313
350
  return undefined;
314
351
  }
352
+ if (kind === "call") {
353
+ const flatPrefix = renderFlatOrUndefined(prefixDoc, options);
354
+ if (flatPrefix === undefined) {
355
+ return undefined;
356
+ }
357
+ prefixDoc = flatPrefix;
358
+ }
315
359
  const printedItems = printItems();
316
360
  const listDoc = [
317
361
  openChar,
@@ -325,6 +369,49 @@ function tryPreserveMultilineList(path, options, print, kind) {
325
369
  ];
326
370
  return [breakParent, prefixDoc, listDoc, suffixDoc];
327
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
+ }
328
415
  function tryHugWrappedCallback(path, options, print) {
329
416
  const { node } = path;
330
417
  if (node.type !== "CallExpression" || node.optional) {
@@ -360,12 +447,16 @@ function tryHugWrappedCallback(path, options, print) {
360
447
  !wouldBreakStandalone(expandDoc, options)) {
361
448
  return undefined;
362
449
  }
450
+ const calleeDoc = renderFlatOrUndefined(print("callee"), options);
451
+ if (calleeDoc === undefined) {
452
+ return undefined;
453
+ }
363
454
  const beforeDocs = printedArgs.slice(0, expandIndex);
364
455
  const afterDocs = printedArgs.slice(expandIndex + 1);
365
456
  const beforeWithCommas = beforeDocs.flatMap((doc) => [doc, ", "]);
366
457
  const afterWithCommas = afterDocs.flatMap((doc) => [", ", doc]);
367
458
  const primaryDoc = [
368
- print("callee"),
459
+ calleeDoc,
369
460
  "(",
370
461
  ...beforeWithCommas,
371
462
  group(expandDoc, { shouldBreak: true }),
@@ -580,6 +671,7 @@ const plugin = {
580
671
  tryPreserveMultilineList(path, options, print, "array") ??
581
672
  tryPreserveMultilineList(path, options, print, "params") ??
582
673
  tryHugWrappedCallback(path, options, print) ??
674
+ tryHugSoleLiteralArg(path, options, print) ??
583
675
  tryHugArrowBody(path, options, print) ??
584
676
  tryHugChainArgument(path, options, print);
585
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.9",
3
+ "version": "0.1.11",
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",
@@ -50,7 +50,7 @@
50
50
  "prettier": ">=3.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^26.3.0",
53
+ "@types/node": "^26.4.0",
54
54
  "prettier": "^3.9.6",
55
55
  "typescript": "^7.0.2"
56
56
  }