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

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
@@ -1,5 +1,19 @@
1
1
  # prettier-plugin-hug-call-arguments
2
2
 
3
+ [![license][license-src]][license-href]
4
+ [![npm version][npm-version-src]][npm-version-href]
5
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
6
+ [![CI][ci-src]][ci-href]
7
+
8
+ [license-src]: https://img.shields.io/badge/license-MIT-brightgreen?&style=flat-square
9
+ [license-href]: https://github.com/Generalsimus/prettier-plugin-hug-call-arguments/blob/main/LICENSE
10
+ [npm-version-src]: https://img.shields.io/npm/v/prettier-plugin-hug-call-arguments?&style=flat-square&logo=npm&logoColor=white&color=CB3837
11
+ [npm-version-href]: https://www.npmjs.com/package/prettier-plugin-hug-call-arguments
12
+ [npm-downloads-src]: https://img.shields.io/npm/dt/prettier-plugin-hug-call-arguments?&style=flat-square&logo=npm&logoColor=white&color=CB3837
13
+ [npm-downloads-href]: https://www.npmjs.com/package/prettier-plugin-hug-call-arguments
14
+ [ci-src]: https://img.shields.io/github/actions/workflow/status/Generalsimus/prettier-plugin-hug-call-arguments/test.yml?branch=main&&style=flat-square
15
+ [ci-href]: https://github.com/Generalsimus/prettier-plugin-hug-call-arguments/actions/workflows/test.yml
16
+
3
17
  Fixes [prettier/prettier#11080](https://github.com/prettier/prettier/issues/11080).
4
18
 
5
19
  Prettier hugs the last argument of a call when it's a function/object/array
@@ -41,10 +55,44 @@ app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
41
55
  }));
42
56
  ```
43
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:
61
+
62
+ ```js
63
+ // input
64
+ db.updateTable('User').set({
65
+ isDeleted: true,
66
+ profilePicture: null,
67
+ username: null,
68
+ }).where('id', '=', user.id).execute();
69
+
70
+ // vanilla Prettier output
71
+ db
72
+ .updateTable("User")
73
+ .set({
74
+ isDeleted: true,
75
+ profilePicture: null,
76
+ username: null,
77
+ })
78
+ .where("id", "=", user.id)
79
+ .execute();
80
+
81
+ // with this plugin
82
+ db.updateTable("User").set({
83
+ isDeleted: true,
84
+ profilePicture: null,
85
+ username: null,
86
+ }).where("id", "=", user.id).execute();
87
+ ```
88
+
44
89
  ## How it works
45
90
 
46
91
  This plugin wraps Prettier's built-in `estree` printer and only overrides the
47
- `print` step for `CallExpression` nodes that match a narrow, specific shape:
92
+ `print` step for `CallExpression` nodes that match one of two narrow, specific
93
+ shapes.
94
+
95
+ **Hugging a call-wrapped callback:**
48
96
 
49
97
  - the callee is a plain identifier or non-computed member chain (`foo`,
50
98
  `a.b.c`) — chained calls (`a().b()`) are left untouched
@@ -54,8 +102,17 @@ This plugin wraps Prettier's built-in `estree` printer and only overrides the
54
102
  - there's no blank line between arguments, no comments on the relevant
55
103
  nodes, and no TS type arguments
56
104
 
57
- Every other node, and every `CallExpression` that doesn't match that shape,
58
- is delegated unchanged to Prettier's original printer.
105
+ **Flattening a method chain:**
106
+
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
113
+
114
+ Every other node, and every `CallExpression` that doesn't match one of these
115
+ shapes, is delegated unchanged to Prettier's original printer.
59
116
 
60
117
  ## Usage
61
118
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import type { Plugin } from "prettier";
2
- /** Minimal structural view of the ESTree/Babel nodes this plugin touches. */
2
+ /**
3
+ * Minimal structural view of the ESTree/Babel nodes this plugin touches.
4
+ *
5
+ * `callee` and `object` are declared as required so `AstPath#call` can chain
6
+ * through them (`path.call(fn, "callee", "object")`) without the type
7
+ * collapsing to `never` — see https://github.com/microsoft/TypeScript's
8
+ * handling of `keyof (T | undefined)`. Every real read of these fields is
9
+ * still guarded by a runtime truthiness check, since plenty of node types
10
+ * don't actually have them.
11
+ */
3
12
  interface ESNode {
4
13
  type: string;
5
14
  start?: number;
@@ -9,8 +18,10 @@ interface ESNode {
9
18
  computed?: boolean;
10
19
  typeArguments?: unknown;
11
20
  typeParameters?: unknown;
12
- callee?: ESNode;
13
- object?: ESNode;
21
+ callee: ESNode;
22
+ object: ESNode;
23
+ property?: ESNode;
24
+ name?: string;
14
25
  arguments?: ESNode[];
15
26
  body?: ESNode;
16
27
  }
package/dist/index.js CHANGED
@@ -181,12 +181,127 @@ function tryHugWrappedCallback(path, options, print) {
181
181
  ]),
182
182
  ];
183
183
  }
184
+ const MAX_CHAIN_LINKS = 6;
185
+ /**
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.
191
+ */
192
+ function collectChainLinks(path, print, depth) {
193
+ if (depth > MAX_CHAIN_LINKS) {
194
+ return undefined;
195
+ }
196
+ const { node } = path;
197
+ if (node.type !== "CallExpression" ||
198
+ node.optional ||
199
+ node.typeArguments ||
200
+ node.typeParameters ||
201
+ hasComment(node)) {
202
+ return undefined;
203
+ }
204
+ const { callee } = node;
205
+ if (!callee ||
206
+ callee.type !== "MemberExpression" ||
207
+ callee.computed ||
208
+ callee.optional ||
209
+ hasComment(callee)) {
210
+ return undefined;
211
+ }
212
+ const { property } = callee;
213
+ if (!property || property.type !== "Identifier" || hasComment(property)) {
214
+ return undefined;
215
+ }
216
+ const link = {
217
+ name: property.name ?? "",
218
+ argsDocs: path.map(print, "arguments"),
219
+ };
220
+ const { object } = callee;
221
+ if (!object || hasComment(object)) {
222
+ return undefined;
223
+ }
224
+ if (object.type === "CallExpression") {
225
+ const inner = path.call((innerPath) => collectChainLinks(innerPath, print, depth + 1), "callee", "object");
226
+ return inner === undefined
227
+ ? undefined
228
+ : { links: [...inner.links, link], baseDoc: inner.baseDoc };
229
+ }
230
+ if (!isSimpleCallee(object)) {
231
+ return undefined;
232
+ }
233
+ const baseDoc = path.call(print, "callee", "object");
234
+ return { links: [link], baseDoc };
235
+ }
236
+ /**
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.
244
+ */
245
+ function fitsWhenFlattened(doc, options) {
246
+ const { formatted } = doc_1.printer.printDocToString(doc, options);
247
+ return formatted
248
+ .split("\n")
249
+ .every((line) => line.length <= options.printWidth);
250
+ }
251
+ /**
252
+ * Handles method chains like `db.updateTable("User").set({ ... }).where(
253
+ * "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.
259
+ */
260
+ function tryHugChainArgument(path, options, print) {
261
+ const { node } = path;
262
+ if (node.type !== "CallExpression" || node.optional) {
263
+ return undefined;
264
+ }
265
+ // Only handle the outermost call of a chain — if this call is itself
266
+ // being member-accessed further, let that outer node make the decision.
267
+ if (path.parent?.type === "MemberExpression") {
268
+ return undefined;
269
+ }
270
+ if (!node.callee ||
271
+ node.callee.type !== "MemberExpression" ||
272
+ node.callee.computed ||
273
+ !node.callee.object ||
274
+ node.callee.object.type !== "CallExpression") {
275
+ return undefined;
276
+ }
277
+ const collected = collectChainLinks(path, print, 0);
278
+ if (!collected || collected.links.length < 2) {
279
+ return undefined;
280
+ }
281
+ 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
+ const flatDoc = [
287
+ baseDoc,
288
+ ...links.map((link) => {
289
+ const joinedArgs = link.argsDocs.flatMap((doc, index) => index === 0 ? [doc] : [", ", doc]);
290
+ return [".", link.name, "(", ...joinedArgs, ")"];
291
+ }),
292
+ ];
293
+ if (!fitsWhenFlattened(flatDoc, options)) {
294
+ return undefined;
295
+ }
296
+ return [breakParent, flatDoc];
297
+ }
184
298
  const plugin = {
185
299
  printers: {
186
300
  estree: {
187
301
  ...estreePrinter,
188
302
  print(path, options, print, args) {
189
- const hugged = tryHugWrappedCallback(path, options, print);
303
+ const hugged = tryHugWrappedCallback(path, options, print) ??
304
+ tryHugChainArgument(path, options, print);
190
305
  return hugged === undefined
191
306
  ? estreePrinter.print(path, options, print, args)
192
307
  : hugged;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-hug-call-arguments",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",