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

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 +11 -3
  2. package/dist/index.js +100 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -127,9 +127,17 @@ shapes.
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.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,39 @@ 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)) {
181
245
  return undefined;
182
246
  }
183
- const headWithCommas = headDocs.flatMap((doc) => [doc, ", "]);
184
- const trailingComma = options.trailingComma === "all" ? "," : "";
185
- const allArgsBrokenOut = () => group([
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"),
186
253
  "(",
187
- indent([hardline, join([",", hardline], printedArgs)]),
188
- trailingComma,
189
- hardline,
254
+ ...beforeWithCommas,
255
+ group(expandDoc, { shouldBreak: true }),
256
+ ...afterWithCommas,
190
257
  ")",
191
- ], { shouldBreak: true });
192
- return [
193
- breakParent,
194
- print("callee"),
195
- conditionalGroup([
196
- ["(", ...headWithCommas, group(lastDoc, { shouldBreak: true }), ")"],
197
- allArgsBrokenOut(),
198
- ]),
199
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];
200
271
  }
201
272
  const MAX_CHAIN_LINKS = 6;
202
273
  /**
@@ -286,22 +357,6 @@ function collectChainLinks(path, print, options, depth) {
286
357
  const baseDoc = path.call(print, "callee", "object");
287
358
  return { links: [link], baseDoc };
288
359
  }
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
360
  /**
306
361
  * Handles method chains like `db.updateTable("User").set({ ... }).where(
307
362
  * "id", "=", user.id).execute()`, where Prettier's default chain printer
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.6",
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",