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

ota-meshi / eslint-plugin-jsonc / 22219409734

20 Feb 2026 09:52AM UTC coverage: 71.429% (+0.4%) from 71.001%
22219409734

push

github

web-flow
feat: improve type definitions (#476)

1034 of 1276 branches covered (81.03%)

Branch coverage included in aggregate %.

380 of 562 new or added lines in 34 files covered. (67.62%)

2 existing lines in 2 files now uncovered.

7461 of 10617 relevant lines covered (70.27%)

78.24 hits per line

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

67.06
/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✔
NEW
10
import type { Rule, AST as ESLintAST } from "eslint";
×
11
import type { AST } from "jsonc-eslint-parser";
×
NEW
12
import type {
×
NEW
13
  JSONCComment,
×
NEW
14
  JSONCSourceCode,
×
NEW
15
  JSONCToken,
×
NEW
16
} from "../language/index.ts";
×
NEW
17
import type { RuleTextEditor } from "@eslint/core";
×
18

×
19
export type AroundTarget =
×
20
  | {
×
NEW
21
      before: JSONCToken;
×
NEW
22
      after: JSONCToken;
×
23
      node?: AST.JSONNode | undefined;
×
24
    }
×
25
  | {
×
NEW
26
      before: JSONCToken;
×
NEW
27
      after: JSONCToken;
×
28
      node?: undefined;
×
29
    };
×
30
type NodeTarget = { node: AST.JSONNode; before?: undefined; after?: undefined };
×
31
type Target = NodeTarget | AroundTarget;
×
32
/**
×
33
 * Fixed by moving the target element down for sorting.
×
34
 */
×
35
export function* fixToMoveDownForSorting(
✔
36
  fixer: RuleTextEditor<JSONCToken | JSONCComment>,
133✔
37
  sourceCode: JSONCSourceCode,
133✔
38
  target: Target,
133✔
39
  to: Target,
133✔
40
): IterableIterator<Rule.Fix> {
133✔
41
  const targetInfo = calcTargetMoveDownInfo(sourceCode, target);
133✔
42

133✔
43
  const toEndInfo = getElementEndInfo(sourceCode, to);
133✔
44

133✔
45
  let { insertCode, removeRanges, hasLeadingComma } = targetInfo;
133✔
46
  if (toEndInfo.trailingComma) {
133✔
47
    if (
51✔
48
      hasLeadingComma &&
51!
NEW
49
      toEndInfo.last.range[1] <= toEndInfo.trailingComma.range[0]
×
50
    ) {
51!
51
      yield fixer.removeRange(toEndInfo.trailingComma.range);
×
52
    }
×
53
    hasLeadingComma = true;
51✔
54
    insertCode = targetInfo.withTrailingComma.insertCode;
51✔
55
    removeRanges = targetInfo.withTrailingComma.removeRanges;
51✔
56
  }
51✔
57

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

133✔
85
  for (const removeRange of removeRanges) {
133✔
86
    yield fixer.removeRange(removeRange);
426✔
87
  }
426✔
88
}
133✔
89

×
90
/**
×
91
 * Fixed by moving the target element up for sorting.
×
92
 */
×
93
export function* fixToMoveUpForSorting(
✔
94
  fixer: RuleTextEditor<JSONCToken | JSONCComment>,
38✔
95
  sourceCode: JSONCSourceCode,
38✔
96
  target: Target,
38✔
97
  to: Target,
38✔
98
): IterableIterator<Rule.Fix> {
38✔
99
  const targetInfo = calcTargetMoveUpInfo(sourceCode, target);
38✔
100

38✔
101
  const toPrevInfo = getPrevElementInfo(sourceCode, to);
38✔
102

38✔
103
  if (
38✔
104
    toPrevInfo.leadingComma &&
38✔
105
    toPrevInfo.prevLast.range[1] <= toPrevInfo.leadingComma.range[0]
22✔
106
  ) {
38!
107
    yield fixer.removeRange(toPrevInfo.leadingComma.range);
×
108
  }
×
109

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

×
127
  for (const removeRange of targetInfo.removeRanges) {
✔
128
    yield fixer.removeRange(removeRange);
×
129
  }
×
130
}
×
131

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

133✔
151
  const endInfo = getElementEndInfo(sourceCode, target);
133✔
152
  const prevInfo = getPrevElementInfo(sourceCode, target);
133✔
153

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

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

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

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

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

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

38✔
227
  const endInfo = getElementEndInfo(sourceCode, target);
38✔
228
  const prevInfo = getPrevElementInfo(sourceCode, target);
38✔
229

38✔
230
  let insertCode: string;
38✔
231

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

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

38✔
265
  return {
38✔
266
    insertCode,
38✔
267
    removeRanges,
38✔
268
  };
38✔
269
}
38✔
270

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

1✔
289
/**
1✔
290
 * Get the last token of the node.
1✔
291
 */
1✔
292
function getLastTokenOfNode(
780✔
NEW
293
  sourceCode: JSONCSourceCode,
×
NEW
294
  node: AST.JSONNode | JSONCToken,
×
NEW
295
): JSONCToken {
×
NEW
296
  let token = sourceCode.getLastToken(node);
×
NEW
297
  let target: JSONCToken | null = token;
×
298
  while (
×
299
    (target = sourceCode.getTokenAfter(token)) &&
✔
300
    isClosingParenToken(target)
×
301
  ) {
✔
302
    token = target;
×
303
  }
×
304
  return token;
×
305
}
×
306

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

427✔
353
  const node = target.node;
427✔
354

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

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

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

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

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

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

123✔
452
  return {
123✔
453
    prevElement,
123✔
454
    leadingComma: endInfo.trailingComma,
123✔
455
    prevLast: endInfo.last,
123✔
456
  };
123✔
457
}
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