prettier-plugin-hug-call-arguments 0.1.4 → 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.
- package/README.md +19 -3
- package/dist/index.js +100 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ Requires Prettier 3.x as a peer dependency.
|
|
|
24
24
|
npm install --save-dev prettier-plugin-hug-call-arguments
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
```sh
|
|
28
|
+
yarn add --dev prettier-plugin-hug-call-arguments
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add --save-dev prettier-plugin-hug-call-arguments
|
|
33
|
+
```
|
|
34
|
+
|
|
27
35
|
Add it to your Prettier config (`.prettierrc`, `.prettierrc.json`,
|
|
28
36
|
`prettier.config.js`, or the `"prettier"` key in `package.json`):
|
|
29
37
|
|
|
@@ -119,9 +127,17 @@ shapes.
|
|
|
119
127
|
|
|
120
128
|
- the callee is a plain identifier or non-computed member chain (`foo`,
|
|
121
129
|
`a.b.c`) — chained calls (`a().b()`) are left untouched
|
|
122
|
-
- the last argument is itself a call whose own last argument is a
|
|
123
|
-
function/object/array literal (looked through recursively
|
|
124
|
-
|
|
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
|
|
125
141
|
- there's no blank line between arguments, no comments on the relevant
|
|
126
142
|
nodes, and no TS type arguments
|
|
127
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,
|
|
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
|
|
155
|
-
if (
|
|
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
|
|
175
|
-
const
|
|
176
|
-
// If the
|
|
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 (
|
|
179
|
-
|
|
180
|
-
!wouldBreakStandalone(
|
|
242
|
+
if (expandDoc === undefined ||
|
|
243
|
+
otherDocs.some((doc) => wouldBreakStandalone(doc, options)) ||
|
|
244
|
+
!wouldBreakStandalone(expandDoc, options)) {
|
|
181
245
|
return undefined;
|
|
182
246
|
}
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
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",
|