prettier-plugin-hug-call-arguments 0.1.5 → 0.1.7

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,16 +120,24 @@ 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` nodes that match one of two narrow, specific
124
- shapes.
123
+ `print` step for `CallExpression` and `ArrowFunctionExpression` nodes that
124
+ match one of a few narrow, specific shapes.
125
125
 
126
126
  **Hugging a call-wrapped callback:**
127
127
 
128
128
  - the callee is a plain identifier or non-computed member chain (`foo`,
129
129
  `a.b.c`) — chained calls (`a().b()`) are left untouched
130
- - the last argument is itself a call whose own last argument is a
131
- function/object/array literal (looked through recursively)
132
- - every earlier argument fits on the opening line
130
+ - either the *last* argument is itself a call whose own last argument is a
131
+ function/object/array literal (looked through recursively, so a chain of
132
+ wrappers like `a(b(cb))` is still found the common `catchAsync(cb)` case),
133
+ or, for a 2-argument call whose second argument is short and simple (a
134
+ literal or identifier), the *first* argument is such a wrapping call (the
135
+ lodash-style `uniqueBy(collection, "key")` shape)
136
+ - the resulting line actually fits `printWidth`; if it doesn't (most often
137
+ because a wrapping call itself sits inside *another* wrapping call), this
138
+ falls back to Prettier's default printing for the outer call — inner calls
139
+ still get their own independent chance to hug once Prettier places them at
140
+ their own, shallower indentation
133
141
  - there's no blank line between arguments, no comments on the relevant
134
142
  nodes, and no TS type arguments
135
143
 
package/dist/index.d.ts CHANGED
@@ -24,6 +24,10 @@ interface ESNode {
24
24
  name?: string;
25
25
  arguments?: ESNode[];
26
26
  body?: ESNode;
27
+ params?: ESNode[];
28
+ async?: boolean;
29
+ returnType?: unknown;
30
+ predicate?: unknown;
27
31
  }
28
32
  declare const plugin: Plugin<ESNode>;
29
33
  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, conditionalGroup, indent, hardline, line, softline, join, ifBreak, breakParent, } = doc_1.builders;
62
+ const { group, indent, line, softline, 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.
@@ -82,9 +82,37 @@ function isHuggableFunction(node) {
82
82
  }
83
83
  return false;
84
84
  }
85
+ /**
86
+ * A short, unbreakable-looking companion argument — the kind lodash-style
87
+ * `uniqueBy(collection, iteratee)` APIs take alongside the huggable
88
+ * collection argument. Deliberately conservative: only literals and plain
89
+ * identifiers qualify, so this plugin never has to guess whether some
90
+ * arbitrary expression will break.
91
+ */
92
+ function isSimpleArg(node) {
93
+ if (!node || hasComment(node)) {
94
+ return false;
95
+ }
96
+ return (node.type === "StringLiteral" ||
97
+ node.type === "NumericLiteral" ||
98
+ node.type === "BooleanLiteral" ||
99
+ node.type === "NullLiteral" ||
100
+ node.type === "Identifier" ||
101
+ node.type === "TemplateLiteral");
102
+ }
85
103
  /**
86
104
  * Like Prettier's internal `couldExpandArg`, but also looks through calls
87
105
  * that merely wrap a huggable function, e.g. `catchAsync(async () => {})`.
106
+ * Only ever looks through the *last* argument, arbitrarily deep — so a chain
107
+ * of wrappers like `a(b(c(cb)))` is still found. Deliberately does *not*
108
+ * also recurse through the lodash-style "first argument wraps, second is
109
+ * simple" shape here: that's only applied at the top, in
110
+ * `findExpandableArgIndex`, for the call actually being printed. Letting it
111
+ * recurse here would also make an *outer* call think it should hug a
112
+ * wrapper two levels down, forcing everything onto one line with no way to
113
+ * tell whether that line will actually fit once it's for real embedded
114
+ * after an assignment, an `await`, or another wrapping call — see
115
+ * `fitsWhenFlattened`'s column-0 blind spot below.
88
116
  */
89
117
  function couldExpandArg(node, depth = 0) {
90
118
  if (!node || depth > 4) {
@@ -104,6 +132,33 @@ function couldExpandArg(node, depth = 0) {
104
132
  }
105
133
  return false;
106
134
  }
135
+ /**
136
+ * Finds which argument of a call is the one worth hugging: the last
137
+ * argument when it's itself a wrapping call that leads to something
138
+ * huggable, or — for a 2-argument call whose second argument is short and
139
+ * simple — the first argument instead. Returns `undefined` when neither
140
+ * shape matches, so the caller falls back to Prettier's default printing.
141
+ */
142
+ function findExpandableArgIndex(args) {
143
+ const isWrappingCall = (arg) => arg !== undefined &&
144
+ !hasComment(arg) &&
145
+ arg.type === "CallExpression" &&
146
+ !arg.optional &&
147
+ Boolean(arg.arguments) &&
148
+ (arg.arguments?.length ?? 0) > 0 &&
149
+ couldExpandArg(arg);
150
+ const last = args.at(-1);
151
+ if (isWrappingCall(last)) {
152
+ return args.length - 1;
153
+ }
154
+ if (args.length === 2) {
155
+ const [first, second] = args;
156
+ if (isWrappingCall(first) && isSimpleArg(second)) {
157
+ return 0;
158
+ }
159
+ }
160
+ return undefined;
161
+ }
107
162
  /**
108
163
  * Only intervene for plain-looking callees (`foo(...)`, `a.b.c(...)`) so we
109
164
  * never fight Prettier's member/call-chain printing (`a().b().c(cb)`).
@@ -139,6 +194,21 @@ function wouldBreakStandalone(doc, options) {
139
194
  }
140
195
  return doc_1.printer.printDocToString(doc, options).formatted.includes("\n");
141
196
  }
197
+ /**
198
+ * Prettier's own doc printer doesn't reliably re-check `printWidth` for
199
+ * plain content that comes *after* a forced break inside a conditionalGroup
200
+ * alternative — this is the same class of limitation their own
201
+ * `isHopefullyShortCallArgument` hack works around (see
202
+ * https://github.com/prettier/prettier/issues/2456). So rather than trust
203
+ * `conditionalGroup` to reject an overflowing candidate on its own, render it
204
+ * standalone and check every line ourselves.
205
+ */
206
+ function fitsWhenFlattened(doc, options) {
207
+ const { formatted } = doc_1.printer.printDocToString(doc, options);
208
+ return formatted
209
+ .split("\n")
210
+ .every((line) => line.length <= options.printWidth);
211
+ }
142
212
  function tryHugWrappedCallback(path, options, print) {
143
213
  const { node } = path;
144
214
  if (node.type !== "CallExpression" || node.optional) {
@@ -151,14 +221,8 @@ function tryHugWrappedCallback(path, options, print) {
151
221
  if (args.length === 0 || !node.callee || !isSimpleCallee(node.callee)) {
152
222
  return undefined;
153
223
  }
154
- const lastArg = args.at(-1);
155
- if (!lastArg ||
156
- hasComment(lastArg) ||
157
- lastArg.type !== "CallExpression" ||
158
- lastArg.optional ||
159
- !lastArg.arguments ||
160
- lastArg.arguments.length === 0 ||
161
- !couldExpandArg(lastArg)) {
224
+ const expandIndex = findExpandableArgIndex(args);
225
+ if (expandIndex === undefined) {
162
226
  return undefined;
163
227
  }
164
228
  for (let i = 0; i < args.length - 1; i++) {
@@ -171,32 +235,95 @@ function tryHugWrappedCallback(path, options, print) {
171
235
  }
172
236
  }
173
237
  const printedArgs = path.map(print, "arguments");
174
- const headDocs = printedArgs.slice(0, -1);
175
- const lastDoc = printedArgs.at(-1);
176
- // If the earlier args don't fit on one line, or the wrapped callback
238
+ const expandDoc = printedArgs[expandIndex];
239
+ const otherDocs = printedArgs.filter((_, i) => i !== expandIndex);
240
+ // If the other args don't fit on one line, or the wrapped callback
177
241
  // wouldn't break anyway, Prettier's default output is already fine.
178
- if (lastDoc === undefined ||
179
- headDocs.some((doc) => wouldBreakStandalone(doc, options)) ||
180
- !wouldBreakStandalone(lastDoc, options)) {
242
+ if (expandDoc === undefined ||
243
+ otherDocs.some((doc) => wouldBreakStandalone(doc, options)) ||
244
+ !wouldBreakStandalone(expandDoc, options)) {
245
+ return undefined;
246
+ }
247
+ const beforeDocs = printedArgs.slice(0, expandIndex);
248
+ const afterDocs = printedArgs.slice(expandIndex + 1);
249
+ const beforeWithCommas = beforeDocs.flatMap((doc) => [doc, ", "]);
250
+ const afterWithCommas = afterDocs.flatMap((doc) => [", ", doc]);
251
+ const primaryDoc = [
252
+ print("callee"),
253
+ "(",
254
+ ...beforeWithCommas,
255
+ group(expandDoc, { shouldBreak: true }),
256
+ ...afterWithCommas,
257
+ ")",
258
+ ];
259
+ // Rendering this standalone starts it at column 0, blind to whatever real
260
+ // prefix (an assignment, an `await`, an outer wrapping call) it's actually
261
+ // embedded after — so this under-counts the true column and can pass a
262
+ // candidate that will genuinely overflow once placed for real. It's still
263
+ // the right conservative default: it correctly rejects the case that
264
+ // matters most (this call's own head arguments are too long to share the
265
+ // opening line), and erring toward Prettier's own default breakout is
266
+ // always a safe fallback.
267
+ if (!fitsWhenFlattened(primaryDoc, options)) {
268
+ return undefined;
269
+ }
270
+ return [breakParent, primaryDoc];
271
+ }
272
+ /**
273
+ * Handles an arrow function with a concise (non-block) body that's itself a
274
+ * call needing multi-line printing, e.g.:
275
+ *
276
+ * const f = (data: Job) => pgBoss.then((boss) => {
277
+ * ...
278
+ * });
279
+ *
280
+ * Prettier hugs a concise body directly after `=>` for several node types
281
+ * (object/array literals, JSX, template literals, ...) but not for a plain
282
+ * `CallExpression` — it always inserts a hardline after `=>` and indents the
283
+ * call onto its own line instead, even when the call's own last argument is
284
+ * already going to hug and break internally regardless. This reproduces
285
+ * Prettier's own arrow-head printing for the narrow, common shape (no type
286
+ * parameters, return type, or predicate; default `arrowParens`) and keeps
287
+ * the body on the same line as `=>` instead.
288
+ */
289
+ function tryHugArrowBody(path, options, print) {
290
+ const { node } = path;
291
+ if (node.type !== "ArrowFunctionExpression" ||
292
+ hasComment(node) ||
293
+ node.typeParameters ||
294
+ node.returnType ||
295
+ node.predicate ||
296
+ options.arrowParens === "avoid") {
297
+ return undefined;
298
+ }
299
+ const body = node.body;
300
+ if (!body || body.type !== "CallExpression" || hasComment(body)) {
301
+ return undefined;
302
+ }
303
+ const bodyDoc = print("body");
304
+ if (!wouldBreakStandalone(bodyDoc, options)) {
181
305
  return undefined;
182
306
  }
183
- const headWithCommas = headDocs.flatMap((doc) => [doc, ", "]);
184
307
  const trailingComma = options.trailingComma === "all" ? "," : "";
185
- const allArgsBrokenOut = () => group([
308
+ const paramsDoc = group([
186
309
  "(",
187
- indent([hardline, join([",", hardline], printedArgs)]),
188
- trailingComma,
189
- hardline,
310
+ indent([softline, join([",", line], path.map(print, "params"))]),
311
+ ifBreak(trailingComma),
312
+ softline,
190
313
  ")",
191
- ], { shouldBreak: true });
192
- return [
193
- breakParent,
194
- print("callee"),
195
- conditionalGroup([
196
- ["(", ...headWithCommas, group(lastDoc, { shouldBreak: true }), ")"],
197
- allArgsBrokenOut(),
198
- ]),
314
+ ]);
315
+ const primaryDoc = [
316
+ node.async ? "async " : "",
317
+ paramsDoc,
318
+ " => ",
319
+ bodyDoc,
199
320
  ];
321
+ // Same column-0 blind spot as `tryHugWrappedCallback` (see its comment) —
322
+ // conservative, but errs toward Prettier's own default when unsure.
323
+ if (!fitsWhenFlattened(primaryDoc, options)) {
324
+ return undefined;
325
+ }
326
+ return [breakParent, primaryDoc];
200
327
  }
201
328
  const MAX_CHAIN_LINKS = 6;
202
329
  /**
@@ -286,22 +413,6 @@ function collectChainLinks(path, print, options, depth) {
286
413
  const baseDoc = path.call(print, "callee", "object");
287
414
  return { links: [link], baseDoc };
288
415
  }
289
- /**
290
- * Each link's own argument list already breaks correctly on its own — via
291
- * Prettier's normal, width-aware group mechanics — because there's no group
292
- * wrapping the whole chain forcing anything. The one thing that mechanism
293
- * can't see is a chain of many short, individually-non-breaking links whose
294
- * *combined* length still overflows `printWidth` (nothing internal to break
295
- * on). So render the assembled candidate standalone and check every line
296
- * before committing to it, falling back to Prettier's own chain layout if
297
- * it doesn't fit.
298
- */
299
- function fitsWhenFlattened(doc, options) {
300
- const { formatted } = doc_1.printer.printDocToString(doc, options);
301
- return formatted
302
- .split("\n")
303
- .every((line) => line.length <= options.printWidth);
304
- }
305
416
  /**
306
417
  * Handles method chains like `db.updateTable("User").set({ ... }).where(
307
418
  * "id", "=", user.id).execute()`, where Prettier's default chain printer
@@ -348,6 +459,7 @@ const plugin = {
348
459
  ...estreePrinter,
349
460
  print(path, options, print, args) {
350
461
  const hugged = tryHugWrappedCallback(path, options, print) ??
462
+ tryHugArrowBody(path, options, print) ??
351
463
  tryHugChainArgument(path, options, print);
352
464
  return hugged === undefined
353
465
  ? estreePrinter.print(path, options, print, args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-hug-call-arguments",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",