prettier-plugin-hug-call-arguments 0.1.1 → 0.1.2

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
@@ -55,9 +55,8 @@ app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
55
55
  }));
56
56
  ```
57
57
 
58
- It also flattens short method chains where exactly one call's argument needs
59
- to break, instead of breaking every `.method()` onto its own line — a common
60
- look with query builders:
58
+ It also flattens method chains instead of breaking every `.method()` onto its
59
+ own line — a common look with query builders:
61
60
 
62
61
  ```js
63
62
  // input
@@ -104,12 +103,18 @@ shapes.
104
103
 
105
104
  **Flattening a method chain:**
106
105
 
107
- - every link is a plain, non-computed, non-optional `.name(...)` call down to
108
- a simple base (an identifier, `this`, or a non-computed member chain)
109
- - exactly one link's arguments need to break zero or more than one falls
110
- back to Prettier's normal chain layout
111
- - the flattened result is rendered and measured against `printWidth`; if any
112
- line would overflow, it falls back to Prettier's normal chain layout too
106
+ - every link is a plain, non-optional `.name(...)` or `[expr](...)` call down
107
+ to a simple base (an identifier, `this`, or a non-computed member chain)
108
+ - each link's own arguments are printed exactly as Prettier would print them
109
+ standalone a single object/array/function argument hugs the parens, a
110
+ multi-argument list breaks onto its own lines only if it doesn't fit — so
111
+ any number of links can break independently, correctly, with no special
112
+ casing needed
113
+ - the whole flattened candidate is still rendered and measured against
114
+ `printWidth` as a final check (catching the one thing per-link breaking
115
+ can't: many short, individually-fitting links whose *combined* length
116
+ still overflows); if any line would overflow, it falls back to Prettier's
117
+ normal chain layout
113
118
 
114
119
  Every other node, and every `CallExpression` that doesn't match one of these
115
120
  shapes, is delegated unchanged to Prettier's original printer.
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ interface ESNode {
20
20
  typeParameters?: unknown;
21
21
  callee: ESNode;
22
22
  object: ESNode;
23
- property?: ESNode;
23
+ property: ESNode;
24
24
  name?: string;
25
25
  arguments?: ESNode[];
26
26
  body?: ESNode;
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, join, breakParent } = doc_1.builders;
62
+ const { group, conditionalGroup, indent, hardline, 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.
@@ -122,6 +122,23 @@ function isSimpleCallee(callee) {
122
122
  function hasBlankLineBetween(originalText, start, end) {
123
123
  return /\n[^\S\n]*\n/.test(originalText.slice(start, end));
124
124
  }
125
+ /**
126
+ * `willBreak` only sees *forced* breaks (a hardline, or a group Prettier
127
+ * already marked `shouldBreak: true` — e.g. an object literal whose first
128
+ * property was already on its own line in the source, via `objectWrap:
129
+ * "preserve"`). A conditional break — an object written on one line that's
130
+ * simply too long to fit — has neither, so `willBreak` misses it. Render the
131
+ * doc standalone and check whether it needed more than one line instead.
132
+ * This only decides *how many* links look like they'll break for our own
133
+ * "exactly one" heuristic; the actual safety net against overflow is
134
+ * `fitsWhenFlattened`, which measures the real assembled doc.
135
+ */
136
+ function wouldBreakStandalone(doc, options) {
137
+ if (willBreak(doc)) {
138
+ return true;
139
+ }
140
+ return doc_1.printer.printDocToString(doc, options).formatted.includes("\n");
141
+ }
125
142
  function tryHugWrappedCallback(path, options, print) {
126
143
  const { node } = path;
127
144
  if (node.type !== "CallExpression" || node.optional) {
@@ -159,8 +176,8 @@ function tryHugWrappedCallback(path, options, print) {
159
176
  // If the earlier args don't fit on one line, or the wrapped callback
160
177
  // wouldn't break anyway, Prettier's default output is already fine.
161
178
  if (lastDoc === undefined ||
162
- headDocs.some((doc) => willBreak(doc)) ||
163
- !willBreak(lastDoc)) {
179
+ headDocs.some((doc) => wouldBreakStandalone(doc, options)) ||
180
+ !wouldBreakStandalone(lastDoc, options)) {
164
181
  return undefined;
165
182
  }
166
183
  const headWithCommas = headDocs.flatMap((doc) => [doc, ", "]);
@@ -183,13 +200,39 @@ function tryHugWrappedCallback(path, options, print) {
183
200
  }
184
201
  const MAX_CHAIN_LINKS = 6;
185
202
  /**
186
- * Walks down `node.callee.object` as long as it's a plain, non-computed,
187
- * non-optional `.name(...)` call, printing each link's arguments along the
188
- * way. Returns `undefined` the moment anything doesn't match that shape
189
- * (computed access, optional chaining, generics, comments, spread callee,
190
- * ...), so the caller can safely fall back to Prettier's own chain printer.
203
+ * Builds the `(...)` doc for one link's argument list the same way a plain,
204
+ * standalone call would print it: a single argument is hugged directly
205
+ * against the parens (so an object/array still expands in place, e.g.
206
+ * `.set({ ... })`, not indented as its own nested line); more than one
207
+ * argument gets Prettier's normal "fits on one line, else one per line"
208
+ * group. Either way the break decision is left to Prettier's regular,
209
+ * width-aware group mechanics — not something this plugin pre-computes.
191
210
  */
192
- function collectChainLinks(path, print, depth) {
211
+ function buildArgsDoc(argsDocs, trailingComma) {
212
+ if (argsDocs.length === 0) {
213
+ return "()";
214
+ }
215
+ const [only] = argsDocs;
216
+ if (argsDocs.length === 1 && only !== undefined) {
217
+ return ["(", only, ")"];
218
+ }
219
+ return group([
220
+ "(",
221
+ indent([softline, join([",", line], argsDocs)]),
222
+ ifBreak(trailingComma),
223
+ softline,
224
+ ")",
225
+ ]);
226
+ }
227
+ /**
228
+ * Walks down `node.callee.object` as long as it's a plain, non-optional
229
+ * `.name(...)` or `[expr](...)` call, printing each link's property access
230
+ * and arguments along the way. Returns `undefined` the moment anything
231
+ * doesn't match that shape (optional chaining, generics, comments, spread
232
+ * callee, ...), so the caller can safely fall back to Prettier's own chain
233
+ * printer.
234
+ */
235
+ function collectChainLinks(path, print, options, depth) {
193
236
  if (depth > MAX_CHAIN_LINKS) {
194
237
  return undefined;
195
238
  }
@@ -204,25 +247,35 @@ function collectChainLinks(path, print, depth) {
204
247
  const { callee } = node;
205
248
  if (!callee ||
206
249
  callee.type !== "MemberExpression" ||
207
- callee.computed ||
208
250
  callee.optional ||
209
251
  hasComment(callee)) {
210
252
  return undefined;
211
253
  }
212
254
  const { property } = callee;
213
- if (!property || property.type !== "Identifier" || hasComment(property)) {
255
+ if (!property || hasComment(property)) {
214
256
  return undefined;
215
257
  }
258
+ let accessDoc;
259
+ if (callee.computed) {
260
+ accessDoc = ["[", path.call(print, "callee", "property"), "]"];
261
+ }
262
+ else if (property.type === "Identifier") {
263
+ accessDoc = [".", property.name ?? ""];
264
+ }
265
+ else {
266
+ return undefined;
267
+ }
268
+ const trailingComma = options.trailingComma === "all" ? "," : "";
216
269
  const link = {
217
- name: property.name ?? "",
218
- argsDocs: path.map(print, "arguments"),
270
+ accessDoc,
271
+ argsDoc: buildArgsDoc(path.map(print, "arguments"), trailingComma),
219
272
  };
220
273
  const { object } = callee;
221
274
  if (!object || hasComment(object)) {
222
275
  return undefined;
223
276
  }
224
277
  if (object.type === "CallExpression") {
225
- const inner = path.call((innerPath) => collectChainLinks(innerPath, print, depth + 1), "callee", "object");
278
+ const inner = path.call((innerPath) => collectChainLinks(innerPath, print, options, depth + 1), "callee", "object");
226
279
  return inner === undefined
227
280
  ? undefined
228
281
  : { links: [...inner.links, link], baseDoc: inner.baseDoc };
@@ -234,13 +287,14 @@ function collectChainLinks(path, print, depth) {
234
287
  return { links: [link], baseDoc };
235
288
  }
236
289
  /**
237
- * Prettier's own doc printer doesn't reliably re-check `printWidth` for
238
- * plain content that comes *after* a forced break inside a conditionalGroup
239
- * alternative this is the same class of limitation their own
240
- * `isHopefullyShortCallArgument` hack works around (see
241
- * https://github.com/prettier/prettier/issues/2456). So rather than trust
242
- * `conditionalGroup` to reject an overflowing flat chain on its own, render
243
- * the candidate standalone and check every line ourselves.
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.
244
298
  */
245
299
  function fitsWhenFlattened(doc, options) {
246
300
  const { formatted } = doc_1.printer.printDocToString(doc, options);
@@ -251,11 +305,12 @@ function fitsWhenFlattened(doc, options) {
251
305
  /**
252
306
  * Handles method chains like `db.updateTable("User").set({ ... }).where(
253
307
  * "id", "=", user.id).execute()`, where Prettier's default chain printer
254
- * breaks every `.method()` onto its own line as soon as one argument (here
255
- * the object passed to `.set`) doesn't fit. When exactly one link in the
256
- * chain needs to break and the rest are short, this keeps the chain flat and
257
- * only lets that one argument expand — falling back to Prettier's own
258
- * chain layout (via `conditionalGroup`) if the flat version doesn't fit.
308
+ * breaks every `.method()` onto its own line as soon as any argument
309
+ * doesn't fit. This instead keeps the whole chain flat — `.foo(...).bar(
310
+ * ...).baz(...)` and lets each link's own arguments break independently
311
+ * if they need to, falling back to Prettier's normal chain layout only if
312
+ * the flattened result doesn't fit the shape this plugin handles, or would
313
+ * overflow `printWidth`.
259
314
  */
260
315
  function tryHugChainArgument(path, options, print) {
261
316
  const { node } = path;
@@ -269,31 +324,23 @@ function tryHugChainArgument(path, options, print) {
269
324
  }
270
325
  if (!node.callee ||
271
326
  node.callee.type !== "MemberExpression" ||
272
- node.callee.computed ||
273
327
  !node.callee.object ||
274
328
  node.callee.object.type !== "CallExpression") {
275
329
  return undefined;
276
330
  }
277
- const collected = collectChainLinks(path, print, 0);
331
+ const collected = collectChainLinks(path, print, options, 0);
278
332
  if (!collected || collected.links.length < 2) {
279
333
  return undefined;
280
334
  }
281
335
  const { links, baseDoc } = collected;
282
- const breakingLinks = links.filter((link) => link.argsDocs.some((doc) => willBreak(doc)));
283
- if (breakingLinks.length !== 1) {
284
- return undefined;
285
- }
286
336
  const flatDoc = [
287
337
  baseDoc,
288
- ...links.map((link) => {
289
- const joinedArgs = link.argsDocs.flatMap((doc, index) => index === 0 ? [doc] : [", ", doc]);
290
- return [".", link.name, "(", ...joinedArgs, ")"];
291
- }),
338
+ ...links.map((link) => [link.accessDoc, link.argsDoc]),
292
339
  ];
293
340
  if (!fitsWhenFlattened(flatDoc, options)) {
294
341
  return undefined;
295
342
  }
296
- return [breakParent, flatDoc];
343
+ return willBreak(flatDoc) ? [breakParent, flatDoc] : flatDoc;
297
344
  }
298
345
  const plugin = {
299
346
  printers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-hug-call-arguments",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",