• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ota-meshi / eslint-plugin-jsonc / 22181448961

19 Feb 2026 12:17PM UTC coverage: 73.221% (+28.0%) from 45.195%
22181448961

push

github

web-flow
Convert to ESM-only package with tsdown bundling (#469)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com>
Co-authored-by: yosuke ota <otameshiyo23@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

949 of 1185 branches covered (80.08%)

Branch coverage included in aggregate %.

133 of 141 new or added lines in 11 files covered. (94.33%)

2429 existing lines in 30 files now uncovered.

6841 of 9454 relevant lines covered (72.36%)

48.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.51
/lib/utils/fix-sort-elements.ts
1
import {
9✔
2
  isClosingBraceToken,
1✔
3
  isClosingBracketToken,
1✔
4
  isClosingParenToken,
1✔
5
  isCommaToken,
1✔
6
  isCommentToken,
1✔
7
  isNotCommaToken,
1✔
8
  isOpeningParenToken,
1✔
9
} from "@eslint-community/eslint-utils";
1✔
UNCOV
10
import type { Rule, AST as ESLintAST, SourceCode } from "eslint";
×
UNCOV
11
import type { AST } from "jsonc-eslint-parser";
×
UNCOV
12
import type * as ESTree from "estree";
×
UNCOV
13

×
UNCOV
14
export type AroundTarget =
×
UNCOV
15
  | {
×
UNCOV
16
      before: ESLintAST.Token;
×
UNCOV
17
      after: ESLintAST.Token;
×
UNCOV
18
      node?: AST.JSONNode | undefined;
×
UNCOV
19
    }
×
UNCOV
20
  | {
×
UNCOV
21
      before: ESLintAST.Token;
×
UNCOV
22
      after: ESLintAST.Token;
×
UNCOV
23
      node?: undefined;
×
UNCOV
24
    };
×
UNCOV
25
type NodeTarget = { node: AST.JSONNode; before?: undefined; after?: undefined };
×
26
type Target = NodeTarget | AroundTarget;
×
UNCOV
27
/**
×
UNCOV
28
 * Fixed by moving the target element down for sorting.
×
UNCOV
29
 */
×
UNCOV
30
export function* fixToMoveDownForSorting(
✔
31
  fixer: Rule.RuleFixer,
133✔
32
  sourceCode: SourceCode,
133✔
33
  target: Target,
133✔
34
  to: Target,
133✔
35
): IterableIterator<Rule.Fix> {
133✔
36
  const targetInfo = calcTargetMoveDownInfo(sourceCode, target);
133✔
37

133✔
38
  const toEndInfo = getElementEndInfo(sourceCode, to);
133✔
39

133✔
40
  let { insertCode, removeRanges, hasLeadingComma } = targetInfo;
133✔
41
  if (toEndInfo.trailingComma) {
133✔
42
    if (
51✔
43
      hasLeadingComma &&
51!
UNCOV
44
      toEndInfo.last.range![1] <= toEndInfo.trailingComma.range[0]
×
45
    ) {
51!
UNCOV
46
      yield fixer.removeRange(toEndInfo.trailingComma.range);
×
UNCOV
47
    }
×
48
    hasLeadingComma = true;
51✔
49
    insertCode = targetInfo.withTrailingComma.insertCode;
51✔
50
    removeRanges = targetInfo.withTrailingComma.removeRanges;
51✔
51
  }
51✔
52

133✔
53
  let insertRange = [
133✔
54
    toEndInfo.last.range![1],
133✔
55
    toEndInfo.last.range![1],
133✔
56
  ] as ESLintAST.Range;
133✔
57
  const toNextToken = sourceCode.getTokenAfter(toEndInfo.last, {
133✔
58
    includeComments: true,
133✔
59
  })!;
133✔
60
  if (toNextToken.loc!.start.line - toEndInfo.last.loc!.end.line > 1) {
133✔
61
    // If there are blank lines, the element is inserted after the blank lines.
2✔
62
    const offset = sourceCode.getIndexFromLoc({
2✔
63
      line: toNextToken.loc!.start.line - 1,
2✔
64
      column: 0,
2✔
65
    });
2✔
66
    insertRange = [offset, offset];
2✔
67
  }
2✔
68
  if (!hasLeadingComma) {
133✔
69
    if (to.node) {
82✔
70
      yield fixer.insertTextAfterRange(
82✔
71
        getLastTokenOfNode(sourceCode, to.node).range,
82✔
72
        ",",
82✔
73
      );
82✔
74
    } else {
82!
UNCOV
75
      yield fixer.insertTextBeforeRange(to.after.range, ",");
×
UNCOV
76
    }
×
77
  }
82✔
78
  yield fixer.insertTextAfterRange(insertRange, insertCode);
133✔
79

133✔
80
  for (const removeRange of removeRanges) {
133✔
81
    yield fixer.removeRange(removeRange);
426✔
82
  }
426✔
83
}
133✔
UNCOV
84

×
UNCOV
85
/**
×
UNCOV
86
 * Fixed by moving the target element up for sorting.
×
UNCOV
87
 */
×
UNCOV
88
export function* fixToMoveUpForSorting(
✔
89
  fixer: Rule.RuleFixer,
38✔
90
  sourceCode: SourceCode,
38✔
91
  target: Target,
38✔
92
  to: Target,
38✔
93
): IterableIterator<Rule.Fix> {
38✔
94
  const targetInfo = calcTargetMoveUpInfo(sourceCode, target);
38✔
95

38✔
96
  const toPrevInfo = getPrevElementInfo(sourceCode, to);
38✔
97

38✔
98
  if (
38✔
99
    toPrevInfo.leadingComma &&
38✔
100
    toPrevInfo.prevLast.range![1] <= toPrevInfo.leadingComma.range[0]
22✔
101
  ) {
38!
UNCOV
102
    yield fixer.removeRange(toPrevInfo.leadingComma.range);
×
UNCOV
103
  }
×
104

38✔
105
  let insertRange = [
38✔
106
    toPrevInfo.prevLast.range![1],
38✔
107
    toPrevInfo.prevLast.range![1],
38✔
108
  ] as ESLintAST.Range;
38✔
109
  const toBeforeNextToken = sourceCode.getTokenAfter(toPrevInfo.prevLast, {
38✔
UNCOV
110
    includeComments: true,
×
UNCOV
111
  })!;
×
UNCOV
112
  if (
×
UNCOV
113
    toBeforeNextToken.loc!.start.line - toPrevInfo.prevLast.loc!.end.line >
×
UNCOV
114
    1
×
UNCOV
115
  ) {
×
UNCOV
116
    // If there are blank lines, the element is inserted after the blank lines.
×
UNCOV
117
    const offset = sourceCode.getIndexFromLoc({
×
UNCOV
118
      line: toBeforeNextToken.loc!.start.line - 1,
×
UNCOV
119
      column: 0,
×
UNCOV
120
    });
×
UNCOV
121
    insertRange = [offset, offset];
×
UNCOV
122
  }
×
UNCOV
123
  yield fixer.insertTextAfterRange(insertRange, targetInfo.insertCode);
×
UNCOV
124

×
UNCOV
125
  for (const removeRange of targetInfo.removeRanges) {
✔
UNCOV
126
    yield fixer.removeRange(removeRange);
×
UNCOV
127
  }
×
UNCOV
128
}
×
UNCOV
129

×
UNCOV
130
/**
×
UNCOV
131
 * Calculate the fix information of the target element to be moved down for sorting.
×
UNCOV
132
 */
×
133
function calcTargetMoveDownInfo(
133✔
134
  sourceCode: SourceCode,
133✔
135
  target: Target,
133✔
136
): {
133✔
137
  insertCode: string;
133✔
138
  removeRanges: ESLintAST.Range[];
133✔
139
  hasLeadingComma: boolean;
133✔
140
  withTrailingComma: {
133✔
141
    insertCode: string;
133✔
142
    removeRanges: ESLintAST.Range[];
133✔
143
  };
133✔
144
} {
133✔
145
  const nodeStartIndex = target.node
133✔
146
    ? getFirstTokenOfNode(sourceCode, target.node).range[0]
133!
UNCOV
147
    : target.before.range[1];
×
148

133✔
149
  const endInfo = getElementEndInfo(sourceCode, target);
133✔
150
  const prevInfo = getPrevElementInfo(sourceCode, target);
133✔
151

133✔
152
  let insertCode = sourceCode.text.slice(
133✔
153
    prevInfo.prevLast.range![1],
133✔
154
    nodeStartIndex,
133✔
155
  );
133✔
156
  const removeRanges: ESLintAST.Range[] = [
133✔
157
    [prevInfo.prevLast.range![1], nodeStartIndex],
133✔
158
  ];
133✔
159
  const hasLeadingComma =
133✔
160
    prevInfo.leadingComma &&
133✔
161
    prevInfo.prevLast.range![1] <= prevInfo.leadingComma.range[0];
64✔
162

133✔
163
  let withTrailingComma: {
133✔
164
    insertCode: string;
133✔
165
    removeRanges: ESLintAST.Range[];
133✔
166
  };
133✔
167

133✔
168
  const suffixRange: ESLintAST.Range = [nodeStartIndex, endInfo.last.range![1]];
133✔
169
  const suffix = sourceCode.text.slice(...suffixRange);
133✔
170
  if (
133✔
171
    endInfo.trailingComma &&
133✔
172
    endInfo.trailingComma.range[1] <= endInfo.last.range![1]
133✔
173
  ) {
133✔
174
    withTrailingComma = {
129✔
175
      insertCode: `${insertCode}${suffix}`,
129✔
176
      removeRanges: [...removeRanges, suffixRange],
129✔
UNCOV
177
    };
×
UNCOV
178
    insertCode += `${sourceCode.text.slice(nodeStartIndex, endInfo.trailingComma.range[0])}${sourceCode.text.slice(endInfo.trailingComma.range[1], endInfo.last.range![1])}`;
×
UNCOV
179

×
UNCOV
180
    if (!hasLeadingComma) {
×
UNCOV
181
      if (endInfo.trailingComma) {
×
UNCOV
182
        removeRanges.push(endInfo.trailingComma.range);
×
UNCOV
183
      }
×
UNCOV
184
    }
×
UNCOV
185
    removeRanges.push(
×
UNCOV
186
      [nodeStartIndex, endInfo.trailingComma.range[0]],
×
UNCOV
187
      [endInfo.trailingComma.range[1], endInfo.last.range![1]],
×
UNCOV
188
    );
×
UNCOV
189
  } else {
✔
UNCOV
190
    if (!hasLeadingComma) {
×
UNCOV
191
      if (endInfo.trailingComma) {
×
UNCOV
192
        removeRanges.push(endInfo.trailingComma.range);
×
UNCOV
193
      }
×
UNCOV
194
    }
×
UNCOV
195
    withTrailingComma = {
×
UNCOV
196
      insertCode: `${insertCode},${suffix}`,
×
UNCOV
197
      removeRanges: [...removeRanges, suffixRange],
×
UNCOV
198
    };
×
UNCOV
199
    insertCode += suffix;
×
UNCOV
200
    removeRanges.push(suffixRange);
×
UNCOV
201
  }
×
UNCOV
202

×
UNCOV
203
  return {
×
UNCOV
204
    insertCode,
×
UNCOV
205
    removeRanges,
×
UNCOV
206
    hasLeadingComma: Boolean(hasLeadingComma),
×
UNCOV
207
    withTrailingComma,
×
UNCOV
208
  };
×
UNCOV
209
}
×
UNCOV
210

×
UNCOV
211
/**
×
UNCOV
212
 * Calculate the fix information of the target element to be moved up for sorting.
×
UNCOV
213
 */
×
214
function calcTargetMoveUpInfo(
38✔
215
  sourceCode: SourceCode,
38✔
216
  target: Target,
38✔
217
): {
38✔
218
  insertCode: string;
38✔
219
  removeRanges: ESLintAST.Range[];
38✔
220
} {
38✔
221
  const nodeEndIndex = target.node
38✔
222
    ? getLastTokenOfNode(sourceCode, target.node).range[1]
38!
UNCOV
223
    : target.after.range[0];
×
224

38✔
225
  const endInfo = getElementEndInfo(sourceCode, target);
38✔
226
  const prevInfo = getPrevElementInfo(sourceCode, target);
38✔
227

38✔
228
  let insertCode: string;
38✔
229

38✔
230
  const removeRanges: ESLintAST.Range[] = [];
38✔
231
  if (
38✔
232
    prevInfo.leadingComma &&
38✔
233
    prevInfo.prevLast.range![1] <= prevInfo.leadingComma.range[0]
38✔
234
  ) {
38!
UNCOV
235
    insertCode = `${sourceCode.text.slice(
×
UNCOV
236
      prevInfo.prevLast.range![1],
×
UNCOV
237
      prevInfo.leadingComma.range[0],
×
UNCOV
238
    )}${sourceCode.text.slice(prevInfo.leadingComma.range[1], nodeEndIndex)}`;
×
UNCOV
239
    removeRanges.push(
×
UNCOV
240
      [prevInfo.prevLast.range![1], prevInfo.leadingComma.range[0]],
×
UNCOV
241
      [prevInfo.leadingComma.range[1], nodeEndIndex],
×
UNCOV
242
    );
×
243
  } else {
38✔
244
    insertCode = sourceCode.text.slice(
38✔
245
      prevInfo.prevLast.range![1],
38✔
246
      nodeEndIndex,
38✔
247
    );
38✔
248
    removeRanges.push([prevInfo.prevLast.range![1], nodeEndIndex]);
38✔
249
  }
38✔
250

38✔
251
  const hasTrailingComma =
38✔
252
    endInfo.trailingComma &&
38✔
253
    endInfo.trailingComma.range[1] <= endInfo.last.range![1];
10✔
254
  if (!hasTrailingComma) {
38✔
255
    insertCode += ",";
28✔
256
    if (prevInfo.leadingComma) {
28✔
257
      removeRanges.push(prevInfo.leadingComma.range);
28✔
258
    }
28✔
259
  }
28✔
260
  insertCode += sourceCode.text.slice(nodeEndIndex, endInfo.last.range![1]);
38✔
261
  removeRanges.push([nodeEndIndex, endInfo.last.range![1]]);
38✔
262

38✔
263
  return {
38✔
264
    insertCode,
38✔
265
    removeRanges,
38✔
266
  };
38✔
267
}
38✔
UNCOV
268

×
UNCOV
269
/**
×
UNCOV
270
 * Get the first token of the node.
×
UNCOV
271
 */
×
272
function getFirstTokenOfNode(
342✔
273
  sourceCode: SourceCode,
342✔
274
  node: AST.JSONNode | ESLintAST.Token,
342✔
275
): ESLintAST.Token {
342✔
276
  let token = sourceCode.getFirstToken(node as never)!;
342✔
277
  let target: ESLintAST.Token | null = token;
342✔
278
  while (
342✔
279
    (target = sourceCode.getTokenBefore(token)) &&
342✔
280
    isOpeningParenToken(target)
344✔
281
  ) {
342✔
282
    token = target;
2✔
283
  }
2✔
284
  return token;
342✔
285
}
342✔
286

1✔
287
/**
1✔
288
 * Get the last token of the node.
1✔
289
 */
1✔
290
function getLastTokenOfNode(
780✔
UNCOV
291
  sourceCode: SourceCode,
×
UNCOV
292
  node: AST.JSONNode | ESLintAST.Token,
×
UNCOV
293
): ESLintAST.Token {
×
UNCOV
294
  let token = sourceCode.getLastToken(node as never)!;
×
UNCOV
295
  let target: ESLintAST.Token | null = token;
×
UNCOV
296
  while (
×
UNCOV
297
    (target = sourceCode.getTokenAfter(token)) &&
✔
UNCOV
298
    isClosingParenToken(target)
×
UNCOV
299
  ) {
✔
UNCOV
300
    token = target;
×
UNCOV
301
  }
×
UNCOV
302
  return token;
×
UNCOV
303
}
×
UNCOV
304

×
UNCOV
305
/**
×
UNCOV
306
 * Get the end of the target element and the next element and token information.
×
UNCOV
307
 */
×
308
function getElementEndInfo(
427✔
309
  sourceCode: SourceCode,
427✔
310
  target: Target | { node: ESLintAST.Token },
427✔
311
): {
427✔
312
  // Trailing comma
427✔
313
  trailingComma: ESLintAST.Token | null;
427✔
314
  // Next element token
427✔
315
  nextElement: ESLintAST.Token | null;
427✔
316
  // The last token of the target element
427✔
317
  last: ESLintAST.Token | ESTree.Comment;
427✔
318
} {
427✔
319
  const afterToken = target.node
427✔
320
    ? sourceCode.getTokenAfter(getLastTokenOfNode(sourceCode, target.node))!
427!
UNCOV
321
    : target.after;
×
322
  if (isNotCommaToken(afterToken)) {
427✔
323
    // If there is no comma, the element is the last element.
110✔
324
    return {
110✔
325
      trailingComma: null,
110✔
326
      nextElement: null,
110✔
327
      last: getLastTokenWithTrailingComments(sourceCode, target),
110✔
328
    };
110✔
329
  }
110✔
330
  const comma = afterToken;
427✔
331
  const nextElement = sourceCode.getTokenAfter(afterToken)!;
317✔
332
  if (isCommaToken(nextElement)) {
427✔
333
    // If the next element is empty,
1✔
334
    // the position of the comma is the end of the element's range.
1✔
335
    return {
1✔
336
      trailingComma: comma,
1✔
337
      nextElement: null,
1✔
338
      last: comma,
1✔
339
    };
1✔
340
  }
1✔
341
  if (isClosingBraceToken(nextElement) || isClosingBracketToken(nextElement)) {
427✔
342
    // If the next token is a closing brace or bracket,
10✔
343
    // the position of the comma is the end of the element's range.
10✔
344
    return {
10✔
345
      trailingComma: comma,
10✔
346
      nextElement: null,
10✔
347
      last: getLastTokenWithTrailingComments(sourceCode, target),
10✔
348
    };
10✔
349
  }
10✔
350

427✔
351
  const node = target.node;
427✔
352

306✔
353
  if (node && node.loc.end.line === nextElement.loc.start.line) {
427✔
354
    // There is no line break between the target element and the next element.
191✔
355
    return {
191✔
356
      trailingComma: comma,
191✔
357
      nextElement,
191✔
358
      last: comma,
191✔
359
    };
191✔
360
  }
191✔
361
  // There are line breaks between the target element and the next element.
427✔
362
  if (
427✔
363
    node &&
115✔
364
    node.loc.end.line < comma.loc.start.line &&
115✔
365
    comma.loc.end.line < nextElement.loc.start.line
6✔
366
  ) {
427✔
367
    // If there is a line break between the target element and a comma and the next element,
2✔
368
    // the position of the comma is the end of the element's range.
2✔
369
    return {
2✔
370
      trailingComma: comma,
2✔
371
      nextElement,
2✔
372
      last: comma,
2✔
373
    };
2✔
374
  }
2✔
375

427✔
376
  return {
427✔
377
    trailingComma: comma,
113✔
378
    nextElement,
113✔
379
    last: getLastTokenWithTrailingComments(sourceCode, target),
113✔
380
  };
113✔
381
}
427✔
UNCOV
382

×
UNCOV
383
/**
×
UNCOV
384
 * Get the last token of the target element with trailing comments.
×
UNCOV
385
 */
×
386
function getLastTokenWithTrailingComments(
233✔
387
  sourceCode: SourceCode,
233✔
388
  target: Target | { node: ESLintAST.Token },
233✔
389
) {
233✔
390
  if (!target.node) {
233!
UNCOV
391
    return sourceCode.getTokenBefore(target.after, {
×
UNCOV
392
      includeComments: true,
×
UNCOV
393
    })!;
×
UNCOV
394
  }
×
395
  const node = target.node;
233✔
396
  let last: ESLintAST.Token | ESTree.Comment = getLastTokenOfNode(
233✔
397
    sourceCode,
233✔
398
    node,
233✔
399
  );
233✔
400
  let after: ESLintAST.Token | ESTree.Comment | null;
233✔
401
  while (
233✔
402
    (after = sourceCode.getTokenAfter(last, {
233✔
403
      includeComments: true,
233✔
404
    })) &&
233✔
UNCOV
405
    (isCommentToken(after) || isCommaToken(after)) &&
✔
UNCOV
406
    node.loc.end.line === after.loc!.end.line
×
UNCOV
407
  ) {
✔
UNCOV
408
    last = after;
×
UNCOV
409
  }
×
UNCOV
410
  return last;
×
UNCOV
411
}
×
UNCOV
412

×
UNCOV
413
/**
×
UNCOV
414
 * Get the previous element and token information.
×
UNCOV
415
 */
×
416
function getPrevElementInfo(
209✔
417
  sourceCode: SourceCode,
209✔
418
  target: Target,
209✔
419
): {
209✔
420
  // Previous element token
209✔
421
  prevElement: ESLintAST.Token | null;
209✔
422
  // Leading comma
209✔
423
  leadingComma: ESLintAST.Token | null;
209✔
424
  // The last token of the previous element
209✔
425
  prevLast: ESLintAST.Token | ESTree.Comment;
209✔
426
} {
209✔
427
  const beforeToken = target.node
209✔
428
    ? sourceCode.getTokenBefore(getFirstTokenOfNode(sourceCode, target.node))!
209!
UNCOV
429
    : target.before;
×
UNCOV
430
  if (isNotCommaToken(beforeToken)) {
✔
UNCOV
431
    // If there is no comma, the element is the first element.
×
UNCOV
432
    return {
×
UNCOV
433
      prevElement: null,
×
UNCOV
434
      leadingComma: null,
×
UNCOV
435
      prevLast: beforeToken,
×
UNCOV
436
    };
×
UNCOV
437
  }
×
UNCOV
438
  const comma = beforeToken;
✔
UNCOV
439
  const prevElement = sourceCode.getTokenBefore(beforeToken)!;
×
UNCOV
440

×
441
  if (isCommaToken(prevElement)) {
209✔
442
    // If the previous element is empty,
1✔
443
    // the position of the comma is the end of the previous element's range.
1✔
444
    return {
1✔
445
      prevElement: null,
1✔
446
      leadingComma: comma,
1✔
447
      prevLast: comma,
1✔
448
    };
1✔
449
  }
1✔
450

209✔
451
  const endInfo = getElementEndInfo(sourceCode, { node: prevElement });
209✔
452

123✔
453
  return {
123✔
454
    prevElement,
123✔
455
    leadingComma: endInfo.trailingComma,
123✔
456
    prevLast: endInfo.last,
123✔
457
  };
123✔
458
}
209✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc