@so1ve/eslint-plugin-sort-imports 0.115.0 → 0.115.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/src/shared.js CHANGED
@@ -7,60 +7,60 @@ const NEWLINE = /(\r?\n)/;
7
7
  const hasNewline = (string) => NEWLINE.test(string);
8
8
 
9
9
  function guessNewline(sourceCode) {
10
- const match = NEWLINE.exec(sourceCode.text);
10
+ const match = NEWLINE.exec(sourceCode.text);
11
11
 
12
- return match == null ? "\n" : match[0];
12
+ return match == null ? "\n" : match[0];
13
13
  }
14
14
 
15
15
  function parseWhitespace(whitespace) {
16
- const allItems = whitespace.split(NEWLINE);
17
-
18
- // Remove blank lines. `allItems` contains alternating `spaces` (which can be
19
- // the empty string) and `newline` (which is either "\r\n" or "\n"). So in
20
- // practice `allItems` grows like this as there are more newlines in
21
- // `whitespace`:
22
- //
23
- // [spaces]
24
- // [spaces, newline, spaces]
25
- // [spaces, newline, spaces, newline, spaces]
26
- // [spaces, newline, spaces, newline, spaces, newline, spaces]
27
- //
28
- // If there are 5 or more items we have at least one blank line. If so, keep
29
- // the first `spaces`, the first `newline` and the last `spaces`.
30
- const items =
31
- allItems.length >= 5
32
- ? [...allItems.slice(0, 2), ...allItems.slice(-1)]
33
- : allItems;
34
-
35
- return (
36
- items
37
- .map((spacesOrNewline, index) =>
38
- index % 2 === 0
39
- ? { type: "Spaces", code: spacesOrNewline }
40
- : { type: "Newline", code: spacesOrNewline },
41
- )
42
- // Remove empty spaces since it makes debugging easier.
43
- .filter((token) => token.code !== "")
44
- );
16
+ const allItems = whitespace.split(NEWLINE);
17
+
18
+ // Remove blank lines. `allItems` contains alternating `spaces` (which can be
19
+ // the empty string) and `newline` (which is either "\r\n" or "\n"). So in
20
+ // practice `allItems` grows like this as there are more newlines in
21
+ // `whitespace`:
22
+ //
23
+ // [spaces]
24
+ // [spaces, newline, spaces]
25
+ // [spaces, newline, spaces, newline, spaces]
26
+ // [spaces, newline, spaces, newline, spaces, newline, spaces]
27
+ //
28
+ // If there are 5 or more items we have at least one blank line. If so, keep
29
+ // the first `spaces`, the first `newline` and the last `spaces`.
30
+ const items =
31
+ allItems.length >= 5
32
+ ? [...allItems.slice(0, 2), ...allItems.slice(-1)]
33
+ : allItems;
34
+
35
+ return (
36
+ items
37
+ .map((spacesOrNewline, index) =>
38
+ index % 2 === 0
39
+ ? { type: "Spaces", code: spacesOrNewline }
40
+ : { type: "Newline", code: spacesOrNewline },
41
+ )
42
+ // Remove empty spaces since it makes debugging easier.
43
+ .filter((token) => token.code !== "")
44
+ );
45
45
  }
46
46
 
47
47
  const naturalSort = natsort();
48
48
  function compare(path1, path2) {
49
- const path1Depth = path1.split("-").filter((p) => p === "__").length;
50
- const path2Depth = path2.split("-").filter((p) => p === "__").length;
51
- const path1IsDot = path1 === "_-,";
52
- const path2IsDot = path2 === "_-,";
53
-
54
- if (path1IsDot && !path2IsDot) {
55
- return 1;
56
- }
57
- if (path2IsDot && !path1IsDot) {
58
- return -1;
59
- }
60
-
61
- return path1Depth === path2Depth
62
- ? naturalSort(path1, path2)
63
- : path2Depth - path1Depth;
49
+ const path1Depth = path1.split("-").filter((p) => p === "__").length;
50
+ const path2Depth = path2.split("-").filter((p) => p === "__").length;
51
+ const path1IsDot = path1 === "_-,";
52
+ const path2IsDot = path2 === "_-,";
53
+
54
+ if (path1IsDot && !path2IsDot) {
55
+ return 1;
56
+ }
57
+ if (path2IsDot && !path1IsDot) {
58
+ return -1;
59
+ }
60
+
61
+ return path1Depth === path2Depth
62
+ ? naturalSort(path1, path2)
63
+ : path2Depth - path1Depth;
64
64
  }
65
65
 
66
66
  const isIdentifier = (node) => node.type === "Identifier";
@@ -68,7 +68,7 @@ const isIdentifier = (node) => node.type === "Identifier";
68
68
  const isKeyword = (node) => node.type === "Keyword";
69
69
 
70
70
  const isPunctuator = (node, value) =>
71
- node.type === "Punctuator" && node.value === value;
71
+ node.type === "Punctuator" && node.value === value;
72
72
 
73
73
  const isBlockComment = (node) => node.type === "Block";
74
74
 
@@ -79,58 +79,58 @@ const isSpaces = (node) => node.type === "Spaces";
79
79
  const isNewline = (node) => node.type === "Newline";
80
80
 
81
81
  const getImportExportKind = (node) =>
82
- node.importKind || node.exportKind || "value";
82
+ node.importKind || node.exportKind || "value";
83
83
 
84
84
  function getSource(node) {
85
- const source = node.source.value;
86
-
87
- return {
88
- // Sort by directory level rather than by string length.
89
- source: source
90
- // Treat `.` as `./`, `..` as `../`, `../..` as `../../` etc.
91
- .replace(/^[./]*\.$/, "$&/")
92
- // Make `../` sort after `../../` but before `../a` etc.
93
- // Why a comma? See the next comment.
94
- .replace(/^[./]*\/$/, "$&,")
95
- // Make `.` and `/` sort before any other punctation.
96
- // The default order is: _ - , x x x . x x x / x x x
97
- // We’re changing it to: . / , x x x _ x x x - x x x
98
- .replace(/[./_-]/g, (char) => {
99
- switch (char) {
100
- case ".": {
101
- return "_";
102
- }
103
- case "/": {
104
- return "-";
105
- }
106
- case "_": {
107
- return ".";
108
- }
109
- case "-": {
110
- return "/";
111
- }
112
- // istanbul ignore next
113
- default: {
114
- throw new Error(`Unknown source substitution character: ${char}`);
115
- }
116
- }
117
- }),
118
- originalSource: source,
119
- kind: getImportExportKind(node),
120
- };
85
+ const source = node.source.value;
86
+
87
+ return {
88
+ // Sort by directory level rather than by string length.
89
+ source: source
90
+ // Treat `.` as `./`, `..` as `../`, `../..` as `../../` etc.
91
+ .replace(/^[./]*\.$/, "$&/")
92
+ // Make `../` sort after `../../` but before `../a` etc.
93
+ // Why a comma? See the next comment.
94
+ .replace(/^[./]*\/$/, "$&,")
95
+ // Make `.` and `/` sort before any other punctation.
96
+ // The default order is: _ - , x x x . x x x / x x x
97
+ // We’re changing it to: . / , x x x _ x x x - x x x
98
+ .replace(/[./_-]/g, (char) => {
99
+ switch (char) {
100
+ case ".": {
101
+ return "_";
102
+ }
103
+ case "/": {
104
+ return "-";
105
+ }
106
+ case "_": {
107
+ return ".";
108
+ }
109
+ case "-": {
110
+ return "/";
111
+ }
112
+ // istanbul ignore next
113
+ default: {
114
+ throw new Error(`Unknown source substitution character: ${char}`);
115
+ }
116
+ }
117
+ }),
118
+ originalSource: source,
119
+ kind: getImportExportKind(node),
120
+ };
121
121
  }
122
122
 
123
123
  // Like `Array.prototype.findIndex`, but searches from the end.
124
124
  function findLastIndex(array, fn) {
125
- for (let index = array.length - 1; index >= 0; index--) {
126
- if (fn(array[index], index, array)) {
127
- return index;
128
- }
129
- }
130
-
131
- // There are currently no usages of `findLastIndex` where nothing is found.
132
- // istanbul ignore next
133
- return -1;
125
+ for (let index = array.length - 1; index >= 0; index--) {
126
+ if (fn(array[index], index, array)) {
127
+ return index;
128
+ }
129
+ }
130
+
131
+ // There are currently no usages of `findLastIndex` where nothing is found.
132
+ // istanbul ignore next
133
+ return -1;
134
134
  }
135
135
 
136
136
  // Like `Array.prototype.flatMap`, had it been available.
@@ -139,38 +139,38 @@ const flatMap = (array, fn) => array.flatMap(fn);
139
139
  // Returns `sourceCode.getTokens(node)` plus whitespace and comments. All tokens
140
140
  // have a `code` property with `sourceCode.getText(token)`.
141
141
  function getAllTokens(node, sourceCode) {
142
- const tokens = sourceCode.getTokens(node);
143
- const lastTokenIndex = tokens.length - 1;
144
-
145
- return flatMap(tokens, (token, tokenIndex) => {
146
- const newToken = { ...token, code: sourceCode.getText(token) };
147
-
148
- if (tokenIndex === lastTokenIndex) {
149
- return [newToken];
150
- }
151
-
152
- const comments = sourceCode.getCommentsAfter(token);
153
- const last = comments.length > 0 ? comments[comments.length - 1] : token;
154
- const nextToken = tokens[tokenIndex + 1];
155
-
156
- return [
157
- newToken,
158
- ...flatMap(comments, (comment, commentIndex) => {
159
- const previous =
160
- commentIndex === 0 ? token : comments[commentIndex - 1];
161
-
162
- return [
163
- ...parseWhitespace(
164
- sourceCode.text.slice(previous.range[1], comment.range[0]),
165
- ),
166
- { ...comment, code: sourceCode.getText(comment) },
167
- ];
168
- }),
169
- ...parseWhitespace(
170
- sourceCode.text.slice(last.range[1], nextToken.range[0]),
171
- ),
172
- ];
173
- });
142
+ const tokens = sourceCode.getTokens(node);
143
+ const lastTokenIndex = tokens.length - 1;
144
+
145
+ return flatMap(tokens, (token, tokenIndex) => {
146
+ const newToken = { ...token, code: sourceCode.getText(token) };
147
+
148
+ if (tokenIndex === lastTokenIndex) {
149
+ return [newToken];
150
+ }
151
+
152
+ const comments = sourceCode.getCommentsAfter(token);
153
+ const last = comments.length > 0 ? comments[comments.length - 1] : token;
154
+ const nextToken = tokens[tokenIndex + 1];
155
+
156
+ return [
157
+ newToken,
158
+ ...flatMap(comments, (comment, commentIndex) => {
159
+ const previous =
160
+ commentIndex === 0 ? token : comments[commentIndex - 1];
161
+
162
+ return [
163
+ ...parseWhitespace(
164
+ sourceCode.text.slice(previous.range[1], comment.range[0]),
165
+ ),
166
+ { ...comment, code: sourceCode.getText(comment) },
167
+ ];
168
+ }),
169
+ ...parseWhitespace(
170
+ sourceCode.text.slice(last.range[1], nextToken.range[0]),
171
+ ),
172
+ ];
173
+ });
174
174
  }
175
175
 
176
176
  // Prints tokens that are enhanced with a `code` property – like those returned
@@ -178,222 +178,222 @@ function getAllTokens(node, sourceCode) {
178
178
  const printTokens = (tokens) => tokens.map((token) => token.code).join("");
179
179
 
180
180
  const removeBlankLines = (whitespace) =>
181
- printTokens(parseWhitespace(whitespace));
181
+ printTokens(parseWhitespace(whitespace));
182
182
 
183
183
  // `comments` is a list of comments that occur before `node`. Print those and
184
184
  // the whitespace between themselves and between `node`.
185
185
  function printCommentsBefore(node, comments, sourceCode) {
186
- const lastIndex = comments.length - 1;
187
-
188
- return comments
189
- .map((comment, index) => {
190
- const next = index === lastIndex ? node : comments[index + 1];
191
-
192
- return (
193
- sourceCode.getText(comment) +
194
- removeBlankLines(sourceCode.text.slice(comment.range[1], next.range[0]))
195
- );
196
- })
197
- .join("");
186
+ const lastIndex = comments.length - 1;
187
+
188
+ return comments
189
+ .map((comment, index) => {
190
+ const next = index === lastIndex ? node : comments[index + 1];
191
+
192
+ return (
193
+ sourceCode.getText(comment) +
194
+ removeBlankLines(sourceCode.text.slice(comment.range[1], next.range[0]))
195
+ );
196
+ })
197
+ .join("");
198
198
  }
199
199
 
200
200
  // `comments` is a list of comments that occur after `node`. Print those and
201
201
  // the whitespace between themselves and between `node`.
202
202
  const printCommentsAfter = (node, comments, sourceCode) =>
203
- comments
204
- .map((comment, index) => {
205
- const previous = index === 0 ? node : comments[index - 1];
206
-
207
- return (
208
- removeBlankLines(
209
- sourceCode.text.slice(previous.range[1], comment.range[0]),
210
- ) + sourceCode.getText(comment)
211
- );
212
- })
213
- .join("");
203
+ comments
204
+ .map((comment, index) => {
205
+ const previous = index === 0 ? node : comments[index - 1];
206
+
207
+ return (
208
+ removeBlankLines(
209
+ sourceCode.text.slice(previous.range[1], comment.range[0]),
210
+ ) + sourceCode.getText(comment)
211
+ );
212
+ })
213
+ .join("");
214
214
 
215
215
  function getIndentation(node, sourceCode) {
216
- const tokenBefore = sourceCode.getTokenBefore(node, {
217
- includeComments: true,
218
- });
219
- if (tokenBefore == null) {
220
- const text = sourceCode.text.slice(0, node.range[0]);
221
- const lines = text.split(NEWLINE);
222
-
223
- return lines[lines.length - 1];
224
- }
225
- const text = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
226
- const lines = text.split(NEWLINE);
227
-
228
- return lines.length > 1 ? lines[lines.length - 1] : "";
216
+ const tokenBefore = sourceCode.getTokenBefore(node, {
217
+ includeComments: true,
218
+ });
219
+ if (tokenBefore == null) {
220
+ const text = sourceCode.text.slice(0, node.range[0]);
221
+ const lines = text.split(NEWLINE);
222
+
223
+ return lines[lines.length - 1];
224
+ }
225
+ const text = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
226
+ const lines = text.split(NEWLINE);
227
+
228
+ return lines.length > 1 ? lines[lines.length - 1] : "";
229
229
  }
230
230
 
231
231
  function getTrailingSpaces(node, sourceCode) {
232
- const tokenAfter = sourceCode.getTokenAfter(node, {
233
- includeComments: true,
234
- });
235
- if (tokenAfter == null) {
236
- const text = sourceCode.text.slice(node.range[1]);
237
- const lines = text.split(NEWLINE);
238
-
239
- return lines[0];
240
- }
241
- const text = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
242
- const lines = text.split(NEWLINE);
243
-
244
- return lines[0];
232
+ const tokenAfter = sourceCode.getTokenAfter(node, {
233
+ includeComments: true,
234
+ });
235
+ if (tokenAfter == null) {
236
+ const text = sourceCode.text.slice(node.range[1]);
237
+ const lines = text.split(NEWLINE);
238
+
239
+ return lines[0];
240
+ }
241
+ const text = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
242
+ const lines = text.split(NEWLINE);
243
+
244
+ return lines[0];
245
245
  }
246
246
 
247
247
  const sortImportExportItems = (items) =>
248
- [...items].sort((itemA, itemB) =>
249
- // If both items are side effect imports, keep their original order.
250
- itemA.isSideEffectImport && itemB.isSideEffectImport
251
- ? itemA.index - itemB.index
252
- : // If one of the items is a side effect import, move it first.
253
- itemA.isSideEffectImport
254
- ? -1
255
- : itemB.isSideEffectImport
256
- ? 1
257
- : // Compare the `from` part.
258
- compare(itemA.source.source, itemB.source.source) ||
259
- // The `.source` has been slightly tweaked. To stay fully deterministic,
260
- // also sort on the original value.
261
- compare(itemA.source.originalSource, itemB.source.originalSource) ||
262
- // Then put type imports/exports before regular ones.
263
- compare(itemA.source.kind, itemB.source.kind) ||
264
- // Keep the original order if the sources are the same. It’s not worth
265
- // trying to compare anything else, and you can use `import/no-duplicates`
266
- // to get rid of the problem anyway.
267
- itemA.index - itemB.index,
268
- );
248
+ [...items].sort((itemA, itemB) =>
249
+ // If both items are side effect imports, keep their original order.
250
+ itemA.isSideEffectImport && itemB.isSideEffectImport
251
+ ? itemA.index - itemB.index
252
+ : // If one of the items is a side effect import, move it first.
253
+ itemA.isSideEffectImport
254
+ ? -1
255
+ : itemB.isSideEffectImport
256
+ ? 1
257
+ : // Compare the `from` part.
258
+ compare(itemA.source.source, itemB.source.source) ||
259
+ // The `.source` has been slightly tweaked. To stay fully deterministic,
260
+ // also sort on the original value.
261
+ compare(itemA.source.originalSource, itemB.source.originalSource) ||
262
+ // Then put type imports/exports before regular ones.
263
+ compare(itemA.source.kind, itemB.source.kind) ||
264
+ // Keep the original order if the sources are the same. It’s not worth
265
+ // trying to compare anything else, and you can use `import/no-duplicates`
266
+ // to get rid of the problem anyway.
267
+ itemA.index - itemB.index,
268
+ );
269
269
 
270
270
  const sortSpecifierItems = (items) =>
271
- [...items].sort(
272
- (itemA, itemB) =>
273
- // Compare by imported or exported name (external interface name).
274
- // import { a as b } from "a"
275
- // ^
276
- // export { b as a }
277
- // ^
278
- compare(
279
- (itemA.node.imported || itemA.node.exported).name,
280
- (itemB.node.imported || itemB.node.exported).name,
281
- ) ||
282
- // Then compare by the file-local name.
283
- // import { a as b } from "a"
284
- // ^
285
- // export { b as a }
286
- // ^
287
- compare(itemA.node.local.name, itemB.node.local.name) ||
288
- // Then put type specifiers before regular ones.
289
- compare(
290
- getImportExportKind(itemA.node),
291
- getImportExportKind(itemB.node),
292
- ) ||
293
- // Keep the original order if the names are the same. It’s not worth
294
- // trying to compare anything else, `import {a, a} from "mod"` is a syntax
295
- // error anyway (but @babel/eslint-parser kind of supports it).
296
- // istanbul ignore next
297
- itemA.index - itemB.index,
298
- );
271
+ [...items].sort(
272
+ (itemA, itemB) =>
273
+ // Compare by imported or exported name (external interface name).
274
+ // import { a as b } from "a"
275
+ // ^
276
+ // export { b as a }
277
+ // ^
278
+ compare(
279
+ (itemA.node.imported || itemA.node.exported).name,
280
+ (itemB.node.imported || itemB.node.exported).name,
281
+ ) ||
282
+ // Then compare by the file-local name.
283
+ // import { a as b } from "a"
284
+ // ^
285
+ // export { b as a }
286
+ // ^
287
+ compare(itemA.node.local.name, itemB.node.local.name) ||
288
+ // Then put type specifiers before regular ones.
289
+ compare(
290
+ getImportExportKind(itemA.node),
291
+ getImportExportKind(itemB.node),
292
+ ) ||
293
+ // Keep the original order if the names are the same. It’s not worth
294
+ // trying to compare anything else, `import {a, a} from "mod"` is a syntax
295
+ // error anyway (but @babel/eslint-parser kind of supports it).
296
+ // istanbul ignore next
297
+ itemA.index - itemB.index,
298
+ );
299
299
 
300
300
  // A “chunk” is a sequence of statements of a certain type with only comments
301
301
  // and whitespace between.
302
302
  function extractChunks(parentNode, isPartOfChunk) {
303
- const chunks = [];
304
- let chunk = [];
305
- let lastNode;
306
-
307
- for (const node of parentNode.body) {
308
- const result = isPartOfChunk(node, lastNode);
309
- switch (result) {
310
- case "PartOfChunk": {
311
- chunk.push(node);
312
- break;
313
- }
314
-
315
- case "PartOfNewChunk": {
316
- if (chunk.length > 0) {
317
- chunks.push(chunk);
318
- }
319
- chunk = [node];
320
- break;
321
- }
322
-
323
- case "NotPartOfChunk": {
324
- if (chunk.length > 0) {
325
- chunks.push(chunk);
326
- chunk = [];
327
- }
328
- break;
329
- }
330
-
331
- // istanbul ignore next
332
- default: {
333
- throw new Error(`Unknown chunk result: ${result}`);
334
- }
335
- }
336
-
337
- lastNode = node;
338
- }
339
-
340
- if (chunk.length > 0) {
341
- chunks.push(chunk);
342
- }
343
-
344
- return chunks;
303
+ const chunks = [];
304
+ let chunk = [];
305
+ let lastNode;
306
+
307
+ for (const node of parentNode.body) {
308
+ const result = isPartOfChunk(node, lastNode);
309
+ switch (result) {
310
+ case "PartOfChunk": {
311
+ chunk.push(node);
312
+ break;
313
+ }
314
+
315
+ case "PartOfNewChunk": {
316
+ if (chunk.length > 0) {
317
+ chunks.push(chunk);
318
+ }
319
+ chunk = [node];
320
+ break;
321
+ }
322
+
323
+ case "NotPartOfChunk": {
324
+ if (chunk.length > 0) {
325
+ chunks.push(chunk);
326
+ chunk = [];
327
+ }
328
+ break;
329
+ }
330
+
331
+ // istanbul ignore next
332
+ default: {
333
+ throw new Error(`Unknown chunk result: ${result}`);
334
+ }
335
+ }
336
+
337
+ lastNode = node;
338
+ }
339
+
340
+ if (chunk.length > 0) {
341
+ chunks.push(chunk);
342
+ }
343
+
344
+ return chunks;
345
345
  }
346
346
 
347
347
  function maybeReportSorting(context, sorted, start, end) {
348
- const sourceCode = context.getSourceCode();
349
- const original = sourceCode.getText().slice(start, end);
350
- if (original !== sorted) {
351
- context.report({
352
- messageId: "sort",
353
- loc: {
354
- start: sourceCode.getLocFromIndex(start),
355
- end: sourceCode.getLocFromIndex(end),
356
- },
357
- fix: (fixer) => fixer.replaceTextRange([start, end], sorted),
358
- });
359
- }
348
+ const sourceCode = context.getSourceCode();
349
+ const original = sourceCode.getText().slice(start, end);
350
+ if (original !== sorted) {
351
+ context.report({
352
+ messageId: "sort",
353
+ loc: {
354
+ start: sourceCode.getLocFromIndex(start),
355
+ end: sourceCode.getLocFromIndex(end),
356
+ },
357
+ fix: (fixer) => fixer.replaceTextRange([start, end], sorted),
358
+ });
359
+ }
360
360
  }
361
361
 
362
362
  function printSortedItems(sortedItems, originalItems, sourceCode) {
363
- const newline = guessNewline(sourceCode);
364
-
365
- const sorted = sortedItems
366
- .map((groups) =>
367
- groups
368
- .map((groupItems) => groupItems.map((item) => item.code).join(newline))
369
- .join(newline),
370
- )
371
- .join(newline + newline);
372
-
373
- // Edge case: If the last import/export (after sorting) ends with a line
374
- // comment and there’s code (or a multiline block comment) on the same line,
375
- // add a newline so we don’t accidentally comment stuff out.
376
- const flattened = flatMap(sortedItems, (groups) => groups.flat());
377
- const lastSortedItem = flattened[flattened.length - 1];
378
- const lastOriginalItem = originalItems[originalItems.length - 1];
379
- const nextToken = lastSortedItem.needsNewline
380
- ? sourceCode.getTokenAfter(lastOriginalItem.node, {
381
- includeComments: true,
382
- filter: (token) =>
383
- !isLineComment(token) &&
384
- !(
385
- isBlockComment(token) &&
386
- token.loc.end.line === lastOriginalItem.node.loc.end.line
387
- ),
388
- })
389
- : undefined;
390
- const maybeNewline =
391
- nextToken != null &&
392
- nextToken.loc.start.line === lastOriginalItem.node.loc.end.line
393
- ? newline
394
- : "";
395
-
396
- return sorted + maybeNewline;
363
+ const newline = guessNewline(sourceCode);
364
+
365
+ const sorted = sortedItems
366
+ .map((groups) =>
367
+ groups
368
+ .map((groupItems) => groupItems.map((item) => item.code).join(newline))
369
+ .join(newline),
370
+ )
371
+ .join(newline + newline);
372
+
373
+ // Edge case: If the last import/export (after sorting) ends with a line
374
+ // comment and there’s code (or a multiline block comment) on the same line,
375
+ // add a newline so we don’t accidentally comment stuff out.
376
+ const flattened = flatMap(sortedItems, (groups) => groups.flat());
377
+ const lastSortedItem = flattened[flattened.length - 1];
378
+ const lastOriginalItem = originalItems[originalItems.length - 1];
379
+ const nextToken = lastSortedItem.needsNewline
380
+ ? sourceCode.getTokenAfter(lastOriginalItem.node, {
381
+ includeComments: true,
382
+ filter: (token) =>
383
+ !isLineComment(token) &&
384
+ !(
385
+ isBlockComment(token) &&
386
+ token.loc.end.line === lastOriginalItem.node.loc.end.line
387
+ ),
388
+ })
389
+ : undefined;
390
+ const maybeNewline =
391
+ nextToken != null &&
392
+ nextToken.loc.start.line === lastOriginalItem.node.loc.end.line
393
+ ? newline
394
+ : "";
395
+
396
+ return sorted + maybeNewline;
397
397
  }
398
398
 
399
399
  // Wrap the import/export nodes in `passedChunk` in objects with more data about
@@ -401,86 +401,86 @@ function printSortedItems(sortedItems, originalItems, sourceCode) {
401
401
  // the node as a string, with comments (if any). Finding the corresponding
402
402
  // comments is the hard part.
403
403
  function getImportExportItems(
404
- passedChunk,
405
- sourceCode,
406
- isSideEffectImport,
407
- getSpecifiers,
404
+ passedChunk,
405
+ sourceCode,
406
+ isSideEffectImport,
407
+ getSpecifiers,
408
408
  ) {
409
- const chunk = handleLastSemicolon(passedChunk, sourceCode);
410
-
411
- return chunk.map((node, nodeIndex) => {
412
- const lastLine =
413
- nodeIndex === 0
414
- ? node.loc.start.line - 1
415
- : chunk[nodeIndex - 1].loc.end.line;
416
-
417
- // Get all comments before the import/export, except:
418
- //
419
- // - Comments on another line for the first import/export.
420
- // - Comments that belong to the previous import/export (if any) – that is,
421
- // comments that are on the same line as the previous import/export. But
422
- // multiline block comments always belong to this import/export, not the
423
- // previous.
424
- const commentsBefore = sourceCode
425
- .getCommentsBefore(node)
426
- .filter(
427
- (comment) =>
428
- comment.loc.start.line <= node.loc.start.line &&
429
- comment.loc.end.line > lastLine &&
430
- (nodeIndex > 0 || comment.loc.start.line > lastLine),
431
- );
432
-
433
- // Get all comments after the import/export that are on the same line.
434
- // Multiline block comments belong to the _next_ import/export (or the
435
- // following code in case of the last import/export).
436
- const commentsAfter = sourceCode
437
- .getCommentsAfter(node)
438
- .filter((comment) => comment.loc.end.line === node.loc.end.line);
439
-
440
- const before = printCommentsBefore(node, commentsBefore, sourceCode);
441
- const after = printCommentsAfter(node, commentsAfter, sourceCode);
442
-
443
- // Print the indentation before the import/export or its first comment, if
444
- // any, to support indentation in `<script>` tags.
445
- const indentation = getIndentation(
446
- commentsBefore.length > 0 ? commentsBefore[0] : node,
447
- sourceCode,
448
- );
449
-
450
- // Print spaces after the import/export or its last comment, if any, to
451
- // avoid producing a sort error just because you accidentally added a few
452
- // trailing spaces among the imports/exports.
453
- const trailingSpaces = getTrailingSpaces(
454
- commentsAfter.length > 0 ? commentsAfter[commentsAfter.length - 1] : node,
455
- sourceCode,
456
- );
457
-
458
- const code =
459
- indentation +
460
- before +
461
- printWithSortedSpecifiers(node, sourceCode, getSpecifiers) +
462
- after +
463
- trailingSpaces;
464
-
465
- const all = [...commentsBefore, node, ...commentsAfter];
466
- const [start] = all[0].range;
467
- const [, end] = all[all.length - 1].range;
468
-
469
- const source = getSource(node);
470
-
471
- return {
472
- node,
473
- code,
474
- start: start - indentation.length,
475
- end: end + trailingSpaces.length,
476
- isSideEffectImport: isSideEffectImport(node, sourceCode),
477
- source,
478
- index: nodeIndex,
479
- needsNewline:
480
- commentsAfter.length > 0 &&
481
- isLineComment(commentsAfter[commentsAfter.length - 1]),
482
- };
483
- });
409
+ const chunk = handleLastSemicolon(passedChunk, sourceCode);
410
+
411
+ return chunk.map((node, nodeIndex) => {
412
+ const lastLine =
413
+ nodeIndex === 0
414
+ ? node.loc.start.line - 1
415
+ : chunk[nodeIndex - 1].loc.end.line;
416
+
417
+ // Get all comments before the import/export, except:
418
+ //
419
+ // - Comments on another line for the first import/export.
420
+ // - Comments that belong to the previous import/export (if any) – that is,
421
+ // comments that are on the same line as the previous import/export. But
422
+ // multiline block comments always belong to this import/export, not the
423
+ // previous.
424
+ const commentsBefore = sourceCode
425
+ .getCommentsBefore(node)
426
+ .filter(
427
+ (comment) =>
428
+ comment.loc.start.line <= node.loc.start.line &&
429
+ comment.loc.end.line > lastLine &&
430
+ (nodeIndex > 0 || comment.loc.start.line > lastLine),
431
+ );
432
+
433
+ // Get all comments after the import/export that are on the same line.
434
+ // Multiline block comments belong to the _next_ import/export (or the
435
+ // following code in case of the last import/export).
436
+ const commentsAfter = sourceCode
437
+ .getCommentsAfter(node)
438
+ .filter((comment) => comment.loc.end.line === node.loc.end.line);
439
+
440
+ const before = printCommentsBefore(node, commentsBefore, sourceCode);
441
+ const after = printCommentsAfter(node, commentsAfter, sourceCode);
442
+
443
+ // Print the indentation before the import/export or its first comment, if
444
+ // any, to support indentation in `<script>` tags.
445
+ const indentation = getIndentation(
446
+ commentsBefore.length > 0 ? commentsBefore[0] : node,
447
+ sourceCode,
448
+ );
449
+
450
+ // Print spaces after the import/export or its last comment, if any, to
451
+ // avoid producing a sort error just because you accidentally added a few
452
+ // trailing spaces among the imports/exports.
453
+ const trailingSpaces = getTrailingSpaces(
454
+ commentsAfter.length > 0 ? commentsAfter[commentsAfter.length - 1] : node,
455
+ sourceCode,
456
+ );
457
+
458
+ const code =
459
+ indentation +
460
+ before +
461
+ printWithSortedSpecifiers(node, sourceCode, getSpecifiers) +
462
+ after +
463
+ trailingSpaces;
464
+
465
+ const all = [...commentsBefore, node, ...commentsAfter];
466
+ const [start] = all[0].range;
467
+ const [, end] = all[all.length - 1].range;
468
+
469
+ const source = getSource(node);
470
+
471
+ return {
472
+ node,
473
+ code,
474
+ start: start - indentation.length,
475
+ end: end + trailingSpaces.length,
476
+ isSideEffectImport: isSideEffectImport(node, sourceCode),
477
+ source,
478
+ index: nodeIndex,
479
+ needsNewline:
480
+ commentsAfter.length > 0 &&
481
+ isLineComment(commentsAfter[commentsAfter.length - 1]),
482
+ };
483
+ });
484
484
  }
485
485
 
486
486
  // Parsers think that a semicolon after a statement belongs to that statement.
@@ -495,143 +495,143 @@ function getImportExportItems(
495
495
  //
496
496
  // In the above example, the import is adjusted to end after `"x"`.
497
497
  function handleLastSemicolon(chunk, sourceCode) {
498
- const lastIndex = chunk.length - 1;
499
- const lastNode = chunk[lastIndex];
500
- const [nextToLastToken, lastToken] = sourceCode.getLastTokens(lastNode, {
501
- count: 2,
502
- });
503
- const lastIsSemicolon = isPunctuator(lastToken, ";");
504
-
505
- if (!lastIsSemicolon) {
506
- return chunk;
507
- }
508
-
509
- const semicolonBelongsToNode =
510
- nextToLastToken.loc.end.line === lastToken.loc.start.line ||
511
- // If there’s no more code after the last import/export the semicolon has to
512
- // belong to the import/export, even if it is not on the same line.
513
- sourceCode.getTokenAfter(lastToken) == null;
514
-
515
- if (semicolonBelongsToNode) {
516
- return chunk;
517
- }
518
-
519
- // Preserve the start position, but use the end position of the `from` string.
520
- const newLastNode = {
521
- ...lastNode,
522
- range: [lastNode.range[0], nextToLastToken.range[1]],
523
- loc: {
524
- start: lastNode.loc.start,
525
- end: nextToLastToken.loc.end,
526
- },
527
- };
528
-
529
- return [...chunk.slice(0, lastIndex), newLastNode];
498
+ const lastIndex = chunk.length - 1;
499
+ const lastNode = chunk[lastIndex];
500
+ const [nextToLastToken, lastToken] = sourceCode.getLastTokens(lastNode, {
501
+ count: 2,
502
+ });
503
+ const lastIsSemicolon = isPunctuator(lastToken, ";");
504
+
505
+ if (!lastIsSemicolon) {
506
+ return chunk;
507
+ }
508
+
509
+ const semicolonBelongsToNode =
510
+ nextToLastToken.loc.end.line === lastToken.loc.start.line ||
511
+ // If there’s no more code after the last import/export the semicolon has to
512
+ // belong to the import/export, even if it is not on the same line.
513
+ sourceCode.getTokenAfter(lastToken) == null;
514
+
515
+ if (semicolonBelongsToNode) {
516
+ return chunk;
517
+ }
518
+
519
+ // Preserve the start position, but use the end position of the `from` string.
520
+ const newLastNode = {
521
+ ...lastNode,
522
+ range: [lastNode.range[0], nextToLastToken.range[1]],
523
+ loc: {
524
+ start: lastNode.loc.start,
525
+ end: nextToLastToken.loc.end,
526
+ },
527
+ };
528
+
529
+ return [...chunk.slice(0, lastIndex), newLastNode];
530
530
  }
531
531
 
532
532
  function printWithSortedSpecifiers(node, sourceCode, getSpecifiers) {
533
- const allTokens = getAllTokens(node, sourceCode);
534
- const openBraceIndex = allTokens.findIndex((token) =>
535
- isPunctuator(token, "{"),
536
- );
537
- const closeBraceIndex = allTokens.findIndex((token) =>
538
- isPunctuator(token, "}"),
539
- );
540
-
541
- const specifiers = getSpecifiers(node);
542
-
543
- if (
544
- openBraceIndex === -1 ||
545
- closeBraceIndex === -1 ||
546
- specifiers.length <= 1
547
- ) {
548
- return printTokens(allTokens);
549
- }
550
-
551
- const specifierTokens = allTokens.slice(openBraceIndex + 1, closeBraceIndex);
552
- const itemsResult = getSpecifierItems(specifierTokens, sourceCode);
553
-
554
- const items = itemsResult.items.map((originalItem, index) => ({
555
- ...originalItem,
556
- node: specifiers[index],
557
- }));
558
-
559
- const sortedItems = sortSpecifierItems(items);
560
-
561
- const newline = guessNewline(sourceCode);
562
-
563
- // `allTokens[closeBraceIndex - 1]` wouldn’t work because `allTokens` contains
564
- // comments and whitespace.
565
- const hasTrailingComma = isPunctuator(
566
- sourceCode.getTokenBefore(allTokens[closeBraceIndex]),
567
- ",",
568
- );
569
-
570
- const lastIndex = sortedItems.length - 1;
571
- const sorted = flatMap(sortedItems, (item, index) => {
572
- const previous = index === 0 ? undefined : sortedItems[index - 1];
573
-
574
- // Add a newline if the item needs one, unless the previous item (if any)
575
- // already ends with a newline.
576
- const maybeNewline =
577
- previous != null &&
578
- needsStartingNewline(item.before) &&
579
- !(
580
- previous.after.length > 0 &&
581
- isNewline(previous.after[previous.after.length - 1])
582
- )
583
- ? [{ type: "Newline", code: newline }]
584
- : [];
585
-
586
- if (index < lastIndex || hasTrailingComma) {
587
- return [
588
- ...maybeNewline,
589
- ...item.before,
590
- ...item.specifier,
591
- { type: "Comma", code: "," },
592
- ...item.after,
593
- ];
594
- }
595
-
596
- const nonBlankIndex = item.after.findIndex(
597
- (token) => !isNewline(token) && !isSpaces(token),
598
- );
599
-
600
- // Remove whitespace and newlines at the start of `.after` if the item had a
601
- // comma before, but now hasn’t to avoid blank lines and excessive
602
- // whitespace before `}`.
603
- const after = item.hadComma
604
- ? nonBlankIndex === -1
605
- ? []
606
- : item.after.slice(nonBlankIndex)
607
- : item.after;
608
-
609
- return [...maybeNewline, ...item.before, ...item.specifier, ...after];
610
- });
611
-
612
- const maybeNewline =
613
- needsStartingNewline(itemsResult.after) &&
614
- !isNewline(sorted[sorted.length - 1])
615
- ? [{ type: "Newline", code: newline }]
616
- : [];
617
-
618
- return printTokens([
619
- ...allTokens.slice(0, openBraceIndex + 1),
620
- ...itemsResult.before,
621
- ...sorted,
622
- ...maybeNewline,
623
- ...itemsResult.after,
624
- ...allTokens.slice(closeBraceIndex),
625
- ]);
533
+ const allTokens = getAllTokens(node, sourceCode);
534
+ const openBraceIndex = allTokens.findIndex((token) =>
535
+ isPunctuator(token, "{"),
536
+ );
537
+ const closeBraceIndex = allTokens.findIndex((token) =>
538
+ isPunctuator(token, "}"),
539
+ );
540
+
541
+ const specifiers = getSpecifiers(node);
542
+
543
+ if (
544
+ openBraceIndex === -1 ||
545
+ closeBraceIndex === -1 ||
546
+ specifiers.length <= 1
547
+ ) {
548
+ return printTokens(allTokens);
549
+ }
550
+
551
+ const specifierTokens = allTokens.slice(openBraceIndex + 1, closeBraceIndex);
552
+ const itemsResult = getSpecifierItems(specifierTokens, sourceCode);
553
+
554
+ const items = itemsResult.items.map((originalItem, index) => ({
555
+ ...originalItem,
556
+ node: specifiers[index],
557
+ }));
558
+
559
+ const sortedItems = sortSpecifierItems(items);
560
+
561
+ const newline = guessNewline(sourceCode);
562
+
563
+ // `allTokens[closeBraceIndex - 1]` wouldn’t work because `allTokens` contains
564
+ // comments and whitespace.
565
+ const hasTrailingComma = isPunctuator(
566
+ sourceCode.getTokenBefore(allTokens[closeBraceIndex]),
567
+ ",",
568
+ );
569
+
570
+ const lastIndex = sortedItems.length - 1;
571
+ const sorted = flatMap(sortedItems, (item, index) => {
572
+ const previous = index === 0 ? undefined : sortedItems[index - 1];
573
+
574
+ // Add a newline if the item needs one, unless the previous item (if any)
575
+ // already ends with a newline.
576
+ const maybeNewline =
577
+ previous != null &&
578
+ needsStartingNewline(item.before) &&
579
+ !(
580
+ previous.after.length > 0 &&
581
+ isNewline(previous.after[previous.after.length - 1])
582
+ )
583
+ ? [{ type: "Newline", code: newline }]
584
+ : [];
585
+
586
+ if (index < lastIndex || hasTrailingComma) {
587
+ return [
588
+ ...maybeNewline,
589
+ ...item.before,
590
+ ...item.specifier,
591
+ { type: "Comma", code: "," },
592
+ ...item.after,
593
+ ];
594
+ }
595
+
596
+ const nonBlankIndex = item.after.findIndex(
597
+ (token) => !isNewline(token) && !isSpaces(token),
598
+ );
599
+
600
+ // Remove whitespace and newlines at the start of `.after` if the item had a
601
+ // comma before, but now hasn’t to avoid blank lines and excessive
602
+ // whitespace before `}`.
603
+ const after = item.hadComma
604
+ ? nonBlankIndex === -1
605
+ ? []
606
+ : item.after.slice(nonBlankIndex)
607
+ : item.after;
608
+
609
+ return [...maybeNewline, ...item.before, ...item.specifier, ...after];
610
+ });
611
+
612
+ const maybeNewline =
613
+ needsStartingNewline(itemsResult.after) &&
614
+ !isNewline(sorted[sorted.length - 1])
615
+ ? [{ type: "Newline", code: newline }]
616
+ : [];
617
+
618
+ return printTokens([
619
+ ...allTokens.slice(0, openBraceIndex + 1),
620
+ ...itemsResult.before,
621
+ ...sorted,
622
+ ...maybeNewline,
623
+ ...itemsResult.after,
624
+ ...allTokens.slice(closeBraceIndex),
625
+ ]);
626
626
  }
627
627
 
628
628
  const makeEmptyItem = () => ({
629
- // "before" | "specifier" | "after"
630
- state: "before",
631
- before: [],
632
- after: [],
633
- specifier: [],
634
- hadComma: false,
629
+ // "before" | "specifier" | "after"
630
+ state: "before",
631
+ before: [],
632
+ after: [],
633
+ specifier: [],
634
+ hadComma: false,
635
635
  });
636
636
 
637
637
  // Turns a list of tokens between the `{` and `}` of an import/export specifiers
@@ -654,234 +654,234 @@ const makeEmptyItem = () => ({
654
654
  // We have to do carefully preserve all original whitespace this way in order to
655
655
  // be compatible with other stylistic ESLint rules.
656
656
  function getSpecifierItems(tokens) {
657
- const result = {
658
- before: [],
659
- after: [],
660
- items: [],
661
- };
662
-
663
- let current = makeEmptyItem();
664
-
665
- for (const token of tokens) {
666
- switch (current.state) {
667
- case "before": {
668
- switch (token.type) {
669
- case "Newline": {
670
- current.before.push(token);
671
-
672
- // All whitespace and comments before the first newline or
673
- // identifier belong to the `{`, not the first specifier.
674
- if (result.before.length === 0 && result.items.length === 0) {
675
- result.before = current.before;
676
- current = makeEmptyItem();
677
- }
678
- break;
679
- }
680
-
681
- case "Spaces":
682
- case "Block":
683
- case "Line": {
684
- current.before.push(token);
685
- break;
686
- }
687
-
688
- // We’ve reached an identifier.
689
- default: {
690
- // All whitespace and comments before the first newline or
691
- // identifier belong to the `{`, not the first specifier.
692
- if (result.before.length === 0 && result.items.length === 0) {
693
- result.before = current.before;
694
- current = makeEmptyItem();
695
- }
696
-
697
- current.state = "specifier";
698
- current.specifier.push(token);
699
- }
700
- }
701
- break;
702
- }
703
-
704
- case "specifier": {
705
- switch (token.type) {
706
- case "Punctuator": {
707
- // There can only be comma punctuators, but future-proof by checking.
708
- // istanbul ignore else
709
- if (isPunctuator(token, ",")) {
710
- current.hadComma = true;
711
- current.state = "after";
712
- } else {
713
- current.specifier.push(token);
714
- }
715
- break;
716
- }
717
-
718
- // When consuming the specifier part, we eat every token until a comma
719
- // or to the end, basically.
720
- default: {
721
- current.specifier.push(token);
722
- }
723
- }
724
- break;
725
- }
726
-
727
- case "after": {
728
- switch (token.type) {
729
- // Only whitespace and comments after a specifier that are on the same
730
- // belong to the specifier.
731
- case "Newline": {
732
- current.after.push(token);
733
- result.items.push(current);
734
- current = makeEmptyItem();
735
- break;
736
- }
737
-
738
- case "Spaces":
739
- case "Line": {
740
- current.after.push(token);
741
- break;
742
- }
743
-
744
- case "Block": {
745
- // Multiline block comments belong to the next specifier.
746
- if (hasNewline(token.code)) {
747
- result.items.push(current);
748
- current = makeEmptyItem();
749
- current.before.push(token);
750
- } else {
751
- current.after.push(token);
752
- }
753
- break;
754
- }
755
-
756
- // We’ve reached another specifier – time to process that one.
757
- default: {
758
- result.items.push(current);
759
- current = makeEmptyItem();
760
- current.state = "specifier";
761
- current.specifier.push(token);
762
- }
763
- }
764
- break;
765
- }
766
-
767
- // istanbul ignore next
768
- default: {
769
- throw new Error(`Unknown state: ${current.state}`);
770
- }
771
- }
772
- }
773
-
774
- // We’ve reached the end of the tokens. Handle what’s currently in `current`.
775
- switch (current.state) {
776
- // If the last specifier has a trailing comma and some of the remaining
777
- // whitespace and comments are on the same line we end up here. If so we
778
- // want to put that whitespace and comments in `result.after`.
779
- case "before": {
780
- result.after = current.before;
781
- break;
782
- }
783
-
784
- // If the last specifier has no trailing comma we end up here. Move all
785
- // trailing comments and whitespace from `.specifier` to `.after`, and
786
- // comments and whitespace that don’t belong to the specifier to
787
- // `result.after`. The last non-comment and non-whitespace token is usually
788
- // an identifier, but in this case it’s a keyword:
789
- //
790
- // export { z, d as default } from "a"
791
- case "specifier": {
792
- const lastIdentifierIndex = findLastIndex(
793
- current.specifier,
794
- (token2) => isIdentifier(token2) || isKeyword(token2),
795
- );
796
-
797
- const specifier = current.specifier.slice(0, lastIdentifierIndex + 1);
798
- const after = current.specifier.slice(lastIdentifierIndex + 1);
799
-
800
- // If there’s a newline, put everything up to and including (hence the `+
801
- // 1`) that newline in the specifiers’s `.after`.
802
- const newlineIndexRaw = after.findIndex((token2) => isNewline(token2));
803
- const newlineIndex = newlineIndexRaw === -1 ? -1 : newlineIndexRaw + 1;
804
-
805
- // If there’s a multiline block comment, put everything _befor_ that
806
- // comment in the specifiers’s `.after`.
807
- const multilineBlockCommentIndex = after.findIndex(
808
- (token2) => isBlockComment(token2) && hasNewline(token2.code),
809
- );
810
-
811
- const sliceIndex =
812
- // If both a newline and a multiline block comment exists, choose the
813
- // earlier one.
814
- newlineIndex >= 0 && multilineBlockCommentIndex >= 0
815
- ? Math.min(newlineIndex, multilineBlockCommentIndex)
816
- : newlineIndex >= 0
817
- ? newlineIndex
818
- : multilineBlockCommentIndex >= 0
819
- ? multilineBlockCommentIndex
820
- : // If there are no newlines, move the last whitespace into `result.after`.
821
- endsWithSpaces(after)
822
- ? after.length - 1
823
- : -1;
824
-
825
- current.specifier = specifier;
826
- current.after = sliceIndex === -1 ? after : after.slice(0, sliceIndex);
827
- result.items.push(current);
828
- result.after = sliceIndex === -1 ? [] : after.slice(sliceIndex);
829
-
830
- break;
831
- }
832
-
833
- // If the last specifier has a trailing comma and all remaining whitespace
834
- // and comments are on the same line we end up here. If so we want to move
835
- // the final whitespace to `result.after`.
836
- case "after": {
837
- if (endsWithSpaces(current.after)) {
838
- const last = current.after.pop();
839
- result.after = [last];
840
- }
841
- result.items.push(current);
842
- break;
843
- }
844
-
845
- // istanbul ignore next
846
- default: {
847
- throw new Error(`Unknown state: ${current.state}`);
848
- }
849
- }
850
-
851
- return result;
657
+ const result = {
658
+ before: [],
659
+ after: [],
660
+ items: [],
661
+ };
662
+
663
+ let current = makeEmptyItem();
664
+
665
+ for (const token of tokens) {
666
+ switch (current.state) {
667
+ case "before": {
668
+ switch (token.type) {
669
+ case "Newline": {
670
+ current.before.push(token);
671
+
672
+ // All whitespace and comments before the first newline or
673
+ // identifier belong to the `{`, not the first specifier.
674
+ if (result.before.length === 0 && result.items.length === 0) {
675
+ result.before = current.before;
676
+ current = makeEmptyItem();
677
+ }
678
+ break;
679
+ }
680
+
681
+ case "Spaces":
682
+ case "Block":
683
+ case "Line": {
684
+ current.before.push(token);
685
+ break;
686
+ }
687
+
688
+ // We’ve reached an identifier.
689
+ default: {
690
+ // All whitespace and comments before the first newline or
691
+ // identifier belong to the `{`, not the first specifier.
692
+ if (result.before.length === 0 && result.items.length === 0) {
693
+ result.before = current.before;
694
+ current = makeEmptyItem();
695
+ }
696
+
697
+ current.state = "specifier";
698
+ current.specifier.push(token);
699
+ }
700
+ }
701
+ break;
702
+ }
703
+
704
+ case "specifier": {
705
+ switch (token.type) {
706
+ case "Punctuator": {
707
+ // There can only be comma punctuators, but future-proof by checking.
708
+ // istanbul ignore else
709
+ if (isPunctuator(token, ",")) {
710
+ current.hadComma = true;
711
+ current.state = "after";
712
+ } else {
713
+ current.specifier.push(token);
714
+ }
715
+ break;
716
+ }
717
+
718
+ // When consuming the specifier part, we eat every token until a comma
719
+ // or to the end, basically.
720
+ default: {
721
+ current.specifier.push(token);
722
+ }
723
+ }
724
+ break;
725
+ }
726
+
727
+ case "after": {
728
+ switch (token.type) {
729
+ // Only whitespace and comments after a specifier that are on the same
730
+ // belong to the specifier.
731
+ case "Newline": {
732
+ current.after.push(token);
733
+ result.items.push(current);
734
+ current = makeEmptyItem();
735
+ break;
736
+ }
737
+
738
+ case "Spaces":
739
+ case "Line": {
740
+ current.after.push(token);
741
+ break;
742
+ }
743
+
744
+ case "Block": {
745
+ // Multiline block comments belong to the next specifier.
746
+ if (hasNewline(token.code)) {
747
+ result.items.push(current);
748
+ current = makeEmptyItem();
749
+ current.before.push(token);
750
+ } else {
751
+ current.after.push(token);
752
+ }
753
+ break;
754
+ }
755
+
756
+ // We’ve reached another specifier – time to process that one.
757
+ default: {
758
+ result.items.push(current);
759
+ current = makeEmptyItem();
760
+ current.state = "specifier";
761
+ current.specifier.push(token);
762
+ }
763
+ }
764
+ break;
765
+ }
766
+
767
+ // istanbul ignore next
768
+ default: {
769
+ throw new Error(`Unknown state: ${current.state}`);
770
+ }
771
+ }
772
+ }
773
+
774
+ // We’ve reached the end of the tokens. Handle what’s currently in `current`.
775
+ switch (current.state) {
776
+ // If the last specifier has a trailing comma and some of the remaining
777
+ // whitespace and comments are on the same line we end up here. If so we
778
+ // want to put that whitespace and comments in `result.after`.
779
+ case "before": {
780
+ result.after = current.before;
781
+ break;
782
+ }
783
+
784
+ // If the last specifier has no trailing comma we end up here. Move all
785
+ // trailing comments and whitespace from `.specifier` to `.after`, and
786
+ // comments and whitespace that don’t belong to the specifier to
787
+ // `result.after`. The last non-comment and non-whitespace token is usually
788
+ // an identifier, but in this case it’s a keyword:
789
+ //
790
+ // export { z, d as default } from "a"
791
+ case "specifier": {
792
+ const lastIdentifierIndex = findLastIndex(
793
+ current.specifier,
794
+ (token2) => isIdentifier(token2) || isKeyword(token2),
795
+ );
796
+
797
+ const specifier = current.specifier.slice(0, lastIdentifierIndex + 1);
798
+ const after = current.specifier.slice(lastIdentifierIndex + 1);
799
+
800
+ // If there’s a newline, put everything up to and including (hence the `+
801
+ // 1`) that newline in the specifiers’s `.after`.
802
+ const newlineIndexRaw = after.findIndex((token2) => isNewline(token2));
803
+ const newlineIndex = newlineIndexRaw === -1 ? -1 : newlineIndexRaw + 1;
804
+
805
+ // If there’s a multiline block comment, put everything _befor_ that
806
+ // comment in the specifiers’s `.after`.
807
+ const multilineBlockCommentIndex = after.findIndex(
808
+ (token2) => isBlockComment(token2) && hasNewline(token2.code),
809
+ );
810
+
811
+ const sliceIndex =
812
+ // If both a newline and a multiline block comment exists, choose the
813
+ // earlier one.
814
+ newlineIndex >= 0 && multilineBlockCommentIndex >= 0
815
+ ? Math.min(newlineIndex, multilineBlockCommentIndex)
816
+ : newlineIndex >= 0
817
+ ? newlineIndex
818
+ : multilineBlockCommentIndex >= 0
819
+ ? multilineBlockCommentIndex
820
+ : // If there are no newlines, move the last whitespace into `result.after`.
821
+ endsWithSpaces(after)
822
+ ? after.length - 1
823
+ : -1;
824
+
825
+ current.specifier = specifier;
826
+ current.after = sliceIndex === -1 ? after : after.slice(0, sliceIndex);
827
+ result.items.push(current);
828
+ result.after = sliceIndex === -1 ? [] : after.slice(sliceIndex);
829
+
830
+ break;
831
+ }
832
+
833
+ // If the last specifier has a trailing comma and all remaining whitespace
834
+ // and comments are on the same line we end up here. If so we want to move
835
+ // the final whitespace to `result.after`.
836
+ case "after": {
837
+ if (endsWithSpaces(current.after)) {
838
+ const last = current.after.pop();
839
+ result.after = [last];
840
+ }
841
+ result.items.push(current);
842
+ break;
843
+ }
844
+
845
+ // istanbul ignore next
846
+ default: {
847
+ throw new Error(`Unknown state: ${current.state}`);
848
+ }
849
+ }
850
+
851
+ return result;
852
852
  }
853
853
 
854
854
  // If a specifier item starts with a line comment or a singleline block comment
855
855
  // it needs a newline before that. Otherwise that comment can end up belonging
856
856
  // to the _previous_ specifier after sorting.
857
857
  function needsStartingNewline(tokens) {
858
- const before = tokens.filter((token) => !isSpaces(token));
858
+ const before = tokens.filter((token) => !isSpaces(token));
859
859
 
860
- if (before.length === 0) {
861
- return false;
862
- }
860
+ if (before.length === 0) {
861
+ return false;
862
+ }
863
863
 
864
- const firstToken = before[0];
864
+ const firstToken = before[0];
865
865
 
866
- return (
867
- isLineComment(firstToken) ||
868
- (isBlockComment(firstToken) && !hasNewline(firstToken.code))
869
- );
866
+ return (
867
+ isLineComment(firstToken) ||
868
+ (isBlockComment(firstToken) && !hasNewline(firstToken.code))
869
+ );
870
870
  }
871
871
 
872
872
  function endsWithSpaces(tokens) {
873
- const last = tokens.length > 0 ? tokens[tokens.length - 1] : undefined;
873
+ const last = tokens.length > 0 ? tokens[tokens.length - 1] : undefined;
874
874
 
875
- return last == null ? false : isSpaces(last);
875
+ return last == null ? false : isSpaces(last);
876
876
  }
877
877
 
878
878
  module.exports = {
879
- extractChunks,
880
- flatMap,
881
- getImportExportItems,
882
- isPunctuator,
883
- maybeReportSorting,
884
- printSortedItems,
885
- printWithSortedSpecifiers,
886
- sortImportExportItems,
879
+ extractChunks,
880
+ flatMap,
881
+ getImportExportItems,
882
+ isPunctuator,
883
+ maybeReportSorting,
884
+ printSortedItems,
885
+ printWithSortedSpecifiers,
886
+ sortImportExportItems,
887
887
  };