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 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 `ArrowFunctionExpression` nodes that
124
- match one of a few narrow, specific shapes.
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
@@ -28,6 +28,7 @@ interface ESNode {
28
28
  async?: boolean;
29
29
  returnType?: unknown;
30
30
  predicate?: unknown;
31
+ elements?: ESNode[];
31
32
  }
32
33
  declare const plugin: Plugin<ESNode>;
33
34
  export = plugin;
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
- 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
@@ -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
- print("callee"),
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
- const hugged = tryHugWrappedCallback(path, options, print) ??
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.8",
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",