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 +60 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +116 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
/**
|
|
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
|
|
13
|
-
object
|
|
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.
|
|
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",
|