prettier-plugin-hug-call-arguments 0.1.0 → 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 +65 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +166 -4
- 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,43 @@ app.delete("/campgrounds/:id", catchAsync(async (req, res) => {
|
|
|
41
55
|
}));
|
|
42
56
|
```
|
|
43
57
|
|
|
58
|
+
It also flattens method chains instead of breaking every `.method()` onto its
|
|
59
|
+
own line — a common look with query builders:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// input
|
|
63
|
+
db.updateTable('User').set({
|
|
64
|
+
isDeleted: true,
|
|
65
|
+
profilePicture: null,
|
|
66
|
+
username: null,
|
|
67
|
+
}).where('id', '=', user.id).execute();
|
|
68
|
+
|
|
69
|
+
// vanilla Prettier output
|
|
70
|
+
db
|
|
71
|
+
.updateTable("User")
|
|
72
|
+
.set({
|
|
73
|
+
isDeleted: true,
|
|
74
|
+
profilePicture: null,
|
|
75
|
+
username: null,
|
|
76
|
+
})
|
|
77
|
+
.where("id", "=", user.id)
|
|
78
|
+
.execute();
|
|
79
|
+
|
|
80
|
+
// with this plugin
|
|
81
|
+
db.updateTable("User").set({
|
|
82
|
+
isDeleted: true,
|
|
83
|
+
profilePicture: null,
|
|
84
|
+
username: null,
|
|
85
|
+
}).where("id", "=", user.id).execute();
|
|
86
|
+
```
|
|
87
|
+
|
|
44
88
|
## How it works
|
|
45
89
|
|
|
46
90
|
This plugin wraps Prettier's built-in `estree` printer and only overrides the
|
|
47
|
-
`print` step for `CallExpression` nodes that match
|
|
91
|
+
`print` step for `CallExpression` nodes that match one of two narrow, specific
|
|
92
|
+
shapes.
|
|
93
|
+
|
|
94
|
+
**Hugging a call-wrapped callback:**
|
|
48
95
|
|
|
49
96
|
- the callee is a plain identifier or non-computed member chain (`foo`,
|
|
50
97
|
`a.b.c`) — chained calls (`a().b()`) are left untouched
|
|
@@ -54,8 +101,23 @@ This plugin wraps Prettier's built-in `estree` printer and only overrides the
|
|
|
54
101
|
- there's no blank line between arguments, no comments on the relevant
|
|
55
102
|
nodes, and no TS type arguments
|
|
56
103
|
|
|
57
|
-
|
|
58
|
-
|
|
104
|
+
**Flattening a method chain:**
|
|
105
|
+
|
|
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
|
|
118
|
+
|
|
119
|
+
Every other node, and every `CallExpression` that doesn't match one of these
|
|
120
|
+
shapes, is delegated unchanged to Prettier's original printer.
|
|
59
121
|
|
|
60
122
|
## Usage
|
|
61
123
|
|
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
|
@@ -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) =>
|
|
163
|
-
!
|
|
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, ", "]);
|
|
@@ -181,12 +198,157 @@ function tryHugWrappedCallback(path, options, print) {
|
|
|
181
198
|
]),
|
|
182
199
|
];
|
|
183
200
|
}
|
|
201
|
+
const MAX_CHAIN_LINKS = 6;
|
|
202
|
+
/**
|
|
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.
|
|
210
|
+
*/
|
|
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) {
|
|
236
|
+
if (depth > MAX_CHAIN_LINKS) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const { node } = path;
|
|
240
|
+
if (node.type !== "CallExpression" ||
|
|
241
|
+
node.optional ||
|
|
242
|
+
node.typeArguments ||
|
|
243
|
+
node.typeParameters ||
|
|
244
|
+
hasComment(node)) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const { callee } = node;
|
|
248
|
+
if (!callee ||
|
|
249
|
+
callee.type !== "MemberExpression" ||
|
|
250
|
+
callee.optional ||
|
|
251
|
+
hasComment(callee)) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
const { property } = callee;
|
|
255
|
+
if (!property || hasComment(property)) {
|
|
256
|
+
return undefined;
|
|
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" ? "," : "";
|
|
269
|
+
const link = {
|
|
270
|
+
accessDoc,
|
|
271
|
+
argsDoc: buildArgsDoc(path.map(print, "arguments"), trailingComma),
|
|
272
|
+
};
|
|
273
|
+
const { object } = callee;
|
|
274
|
+
if (!object || hasComment(object)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
if (object.type === "CallExpression") {
|
|
278
|
+
const inner = path.call((innerPath) => collectChainLinks(innerPath, print, options, depth + 1), "callee", "object");
|
|
279
|
+
return inner === undefined
|
|
280
|
+
? undefined
|
|
281
|
+
: { links: [...inner.links, link], baseDoc: inner.baseDoc };
|
|
282
|
+
}
|
|
283
|
+
if (!isSimpleCallee(object)) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const baseDoc = path.call(print, "callee", "object");
|
|
287
|
+
return { links: [link], baseDoc };
|
|
288
|
+
}
|
|
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
|
+
/**
|
|
306
|
+
* Handles method chains like `db.updateTable("User").set({ ... }).where(
|
|
307
|
+
* "id", "=", user.id).execute()`, where Prettier's default chain printer
|
|
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`.
|
|
314
|
+
*/
|
|
315
|
+
function tryHugChainArgument(path, options, print) {
|
|
316
|
+
const { node } = path;
|
|
317
|
+
if (node.type !== "CallExpression" || node.optional) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
// Only handle the outermost call of a chain — if this call is itself
|
|
321
|
+
// being member-accessed further, let that outer node make the decision.
|
|
322
|
+
if (path.parent?.type === "MemberExpression") {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
if (!node.callee ||
|
|
326
|
+
node.callee.type !== "MemberExpression" ||
|
|
327
|
+
!node.callee.object ||
|
|
328
|
+
node.callee.object.type !== "CallExpression") {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const collected = collectChainLinks(path, print, options, 0);
|
|
332
|
+
if (!collected || collected.links.length < 2) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const { links, baseDoc } = collected;
|
|
336
|
+
const flatDoc = [
|
|
337
|
+
baseDoc,
|
|
338
|
+
...links.map((link) => [link.accessDoc, link.argsDoc]),
|
|
339
|
+
];
|
|
340
|
+
if (!fitsWhenFlattened(flatDoc, options)) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
return willBreak(flatDoc) ? [breakParent, flatDoc] : flatDoc;
|
|
344
|
+
}
|
|
184
345
|
const plugin = {
|
|
185
346
|
printers: {
|
|
186
347
|
estree: {
|
|
187
348
|
...estreePrinter,
|
|
188
349
|
print(path, options, print, args) {
|
|
189
|
-
const hugged = tryHugWrappedCallback(path, options, print)
|
|
350
|
+
const hugged = tryHugWrappedCallback(path, options, print) ??
|
|
351
|
+
tryHugChainArgument(path, options, print);
|
|
190
352
|
return hugged === undefined
|
|
191
353
|
? estreePrinter.print(path, options, print, args)
|
|
192
354
|
: 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.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",
|