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

ota-meshi / eslint-plugin-jsonc / 12946566952

24 Jan 2025 09:08AM UTC coverage: 72.224% (+0.08%) from 72.146%
12946566952

push

github

web-flow
fix: more improve auto-fix of sort rules (#392)

794 of 1303 branches covered (60.94%)

Branch coverage included in aggregate %.

17 of 18 new or added lines in 1 file covered. (94.44%)

1684 of 2128 relevant lines covered (79.14%)

125.73 hits per line

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

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

14
/**
15
 * Check if the token is a comma.
16
 */
17
function isComma(token: ESLintAST.Token): boolean {
18
  return isCommaToken(token);
919✔
19
}
20

21
/**
22
 * Check if the token is a closing brace.
23
 */
24
function isClosingBrace(token: ESLintAST.Token): boolean {
25
  return isClosingBraceToken(token);
329✔
26
}
27

28
/**
29
 * Check if the token is a closing bracket.
30
 */
31
function isClosingBracket(token: ESLintAST.Token): boolean {
32
  return isClosingBracketToken(token);
319✔
33
}
34

35
export type AroundTarget =
36
  | {
37
      before: ESLintAST.Token;
38
      after: ESLintAST.Token;
39
      node?: AST.JSONNode | undefined;
40
    }
41
  | {
42
      before: ESLintAST.Token;
43
      after: ESLintAST.Token;
44
      node?: undefined;
45
    };
46
type NodeTarget = { node: AST.JSONNode; before?: undefined; after?: undefined };
47
type Target = NodeTarget | AroundTarget;
48
/**
49
 * Fixed target element for sorting.
50
 */
51
export function* fixForSorting(
1✔
52
  fixer: Rule.RuleFixer,
53
  sourceCode: SourceCode,
54
  target: Target,
55
  to: Target,
56
): IterableIterator<Rule.Fix> {
57
  const targetInfo = calcTargetInfo(sourceCode, target);
172✔
58

59
  const toPrevInfo = getPrevElementInfo(sourceCode, to);
172✔
60

61
  if (
172!
62
    toPrevInfo.comma &&
253✔
63
    toPrevInfo.last.range![1] <= toPrevInfo.comma.range[0]
64
  ) {
NEW
65
    yield fixer.removeRange(toPrevInfo.comma.range);
×
66
  }
67

68
  let insertRange = [
172✔
69
    toPrevInfo.last.range![1],
70
    toPrevInfo.last.range![1],
71
  ] as ESLintAST.Range;
72
  const toBeforeNextToken = sourceCode.getTokenAfter(toPrevInfo.last, {
172✔
73
    includeComments: true,
74
  })!;
75
  if (toBeforeNextToken.loc!.start.line - toPrevInfo.last.loc!.end.line > 1) {
172✔
76
    // If there are blank lines, the element is inserted after the blank lines.
77
    const offset = sourceCode.getIndexFromLoc({
8✔
78
      line: toBeforeNextToken.loc!.start.line - 1,
79
      column: 0,
80
    });
81
    insertRange = [offset, offset];
8✔
82
  }
83
  yield fixer.insertTextAfterRange(insertRange, targetInfo.insertCode);
172✔
84

85
  for (const removeRange of targetInfo.removeRanges) {
172✔
86
    yield fixer.removeRange(removeRange);
442✔
87
  }
88
}
89

90
/**
91
 * Calculate the range of the target information.
92
 */
93
function calcTargetInfo(
94
  sourceCode: SourceCode,
95
  target: Target,
96
): {
97
  insertCode: string;
98
  removeRanges: ESLintAST.Range[];
99
} {
100
  if (!target.node) {
172!
101
    return calcTargetInfoFromAround(sourceCode, target);
×
102
  }
103
  const node = target.node;
172✔
104
  const nodeLastToken = getLastTokenOfNode(sourceCode, node);
172✔
105

106
  const endInfo = getElementEndInfo(sourceCode, node);
172✔
107
  const prevInfo = getPrevElementInfo(sourceCode, { node });
172✔
108

109
  let insertCode: string;
110

111
  const removeRanges: ESLintAST.Range[] = [];
172✔
112
  if (prevInfo.comma && prevInfo.last.range![1] <= prevInfo.comma.range[0]) {
172✔
113
    insertCode = `${sourceCode.text.slice(
4✔
114
      prevInfo.last.range![1],
115
      prevInfo.comma.range[0],
116
    )}${sourceCode.text.slice(prevInfo.comma.range[1], nodeLastToken.range[1])}`;
117
    removeRanges.push(
4✔
118
      [prevInfo.last.range![1], prevInfo.comma.range[0]],
119
      [prevInfo.comma.range[1], nodeLastToken.range[1]],
120
    );
121
  } else {
122
    insertCode = sourceCode.text.slice(
168✔
123
      prevInfo.last.range![1],
124
      nodeLastToken.range[1],
125
    );
126
    removeRanges.push([prevInfo.last.range![1], nodeLastToken.range[1]]);
168✔
127
  }
128

129
  const hasTrailingComma =
130
    endInfo.comma && endInfo.comma.range[1] <= endInfo.last.range![1];
172✔
131
  if (!hasTrailingComma) {
172✔
132
    insertCode += ",";
94✔
133
    if (prevInfo.comma) {
94✔
134
      removeRanges.push(prevInfo.comma.range);
94✔
135
    }
136
  }
137
  insertCode += sourceCode.text.slice(
172✔
138
    nodeLastToken.range[1],
139
    endInfo.last.range![1],
140
  );
141
  removeRanges.push([nodeLastToken.range[1], endInfo.last.range![1]]);
172✔
142

143
  return {
172✔
144
    insertCode,
145
    removeRanges,
146
  };
147
}
148

149
/**
150
 * Calculate the range of the target information from the around tokens.
151
 */
152
function calcTargetInfoFromAround(
153
  sourceCode: SourceCode,
154
  target: AroundTarget,
155
): {
156
  insertCode: string;
157
  removeRanges: ESLintAST.Range[];
158
} {
159
  const hasTrailingComma = isComma(target.after);
×
160
  const codeStart = target.before.range[1]; // to include comments
×
161
  let codeEnd: number;
162
  if (hasTrailingComma) {
×
163
    // , /**/,
164
    //  ^^^^^^
165
    codeEnd = target.after.range[1];
×
166
  } else {
167
    // , /**/ ]
168
    //  ^^^^^^
169
    codeEnd = target.after.range[0];
×
170
  }
171
  let removeStart = codeStart;
×
172
  if (!hasTrailingComma) {
×
173
    // The target is always the second or subsequent element, so it always has a leading comma.
174
    // , /**/ ]
175
    // ^^^^^^^
176
    removeStart = target.before.range[0];
×
177
  }
178

179
  return {
×
180
    insertCode:
181
      sourceCode.text.slice(codeStart, codeEnd) + (hasTrailingComma ? "" : ","),
×
182
    removeRanges: [[removeStart, codeEnd]],
183
  };
184
}
185

186
/**
187
 * Get the first token of the node.
188
 */
189
function getFirstTokenOfNode(
190
  sourceCode: SourceCode,
191
  node: AST.JSONNode | ESLintAST.Token,
192
): ESLintAST.Token {
193
  let token = sourceCode.getFirstToken(node as never)!;
344✔
194
  let target: ESLintAST.Token | null = token;
344✔
195
  while (
344✔
196
    (target = sourceCode.getTokenBefore(target)) &&
692✔
197
    isOpeningParenToken(target)
198
  ) {
199
    token = target;
2✔
200
  }
201
  return token;
344✔
202
}
203

204
/**
205
 * Get the last token of the node.
206
 */
207
function getLastTokenOfNode(
208
  sourceCode: SourceCode,
209
  node: AST.JSONNode | ESLintAST.Token,
210
): ESLintAST.Token {
211
  let token = sourceCode.getLastToken(node as never)!;
596✔
212
  let target: ESLintAST.Token | null = token;
596✔
213
  while (
596✔
214
    (target = sourceCode.getTokenAfter(target)) &&
1,196✔
215
    isClosingParenToken(target)
216
  ) {
217
    token = target;
2✔
218
  }
219
  return token;
596✔
220
}
221

222
/**
223
 * Get the end of the target element and the next element and token information.
224
 */
225
function getElementEndInfo(
226
  sourceCode: SourceCode,
227
  node: AST.JSONNode | ESLintAST.Token,
228
): {
229
  // Trailing comma
230
  comma: ESLintAST.Token | null;
231
  // Next element token
232
  nextElement: ESLintAST.Token | null;
233
  // The last token of the target element
234
  last: ESLintAST.Token | ESTree.Comment;
235
} {
236
  const lastToken = getLastTokenOfNode(sourceCode, node);
424✔
237
  const afterToken = sourceCode.getTokenAfter(lastToken)!;
424✔
238
  if (isNotCommaToken(afterToken)) {
424✔
239
    // If there is no comma, the element is the last element.
240
    return {
94✔
241
      comma: null,
242
      nextElement: null,
243
      last: getLastTokenWithTrailingComments(),
244
    };
245
  }
246
  const comma = afterToken;
330✔
247
  const nextElement = sourceCode.getTokenAfter(afterToken)!;
330✔
248
  if (isComma(nextElement)) {
330✔
249
    // If the next element is empty,
250
    // the position of the comma is the end of the element's range.
251
    return {
1✔
252
      comma,
253
      nextElement: null,
254
      last: comma,
255
    };
256
  }
257
  if (isClosingBrace(nextElement) || isClosingBracket(nextElement)) {
329✔
258
    // If the next token is a closing brace or bracket,
259
    // the position of the comma is the end of the element's range.
260
    return {
10✔
261
      comma,
262
      nextElement: null,
263
      last: getLastTokenWithTrailingComments(),
264
    };
265
  }
266

267
  if (node.loc.end.line === nextElement.loc.start.line) {
319✔
268
    // There is no line break between the target element and the next element.
269
    return {
198✔
270
      comma,
271
      nextElement,
272
      last: comma,
273
    };
274
  }
275
  // There are line breaks between the target element and the next element.
276
  if (
121✔
277
    node.loc.end.line < comma.loc.start.line &&
127✔
278
    comma.loc.end.line < nextElement.loc.start.line
279
  ) {
280
    // If there is a line break between the target element and a comma and the next element,
281
    // the position of the comma is the end of the element's range.
282
    return {
2✔
283
      comma,
284
      nextElement,
285
      last: comma,
286
    };
287
  }
288

289
  return {
119✔
290
    comma,
291
    nextElement,
292
    last: getLastTokenWithTrailingComments(),
293
  };
294

295
  /**
296
   * Get the last token of the target element with trailing comments.
297
   */
298
  function getLastTokenWithTrailingComments() {
299
    if (lastToken == null) return afterToken;
223!
300
    let last: ESLintAST.Token | ESTree.Comment = lastToken;
223✔
301
    let after = sourceCode.getTokenAfter(lastToken, {
223✔
302
      includeComments: true,
303
    })!;
304
    while (
223✔
305
      (isCommentToken(after) || isComma(after)) &&
875✔
306
      node.loc.end.line === after.loc!.end.line
307
    ) {
308
      last = after;
151✔
309
      after = sourceCode.getTokenAfter(after, {
151✔
310
        includeComments: true,
311
      })!;
312
    }
313
    return last;
223✔
314
  }
315
}
316

317
/**
318
 * Get the previous element and token information.
319
 */
320
function getPrevElementInfo(
321
  sourceCode: SourceCode,
322
  target: Target,
323
): {
324
  // Leading comma
325
  comma: ESLintAST.Token | null;
326
  // Previous element token
327
  prevElement: ESLintAST.Token | null;
328
  // The last token of the target element
329
  last: ESLintAST.Token | ESTree.Comment;
330
} {
331
  const beforeToken = target.node
344!
332
    ? sourceCode.getTokenBefore(getFirstTokenOfNode(sourceCode, target.node))!
333
    : target.before;
334
  if (isNotCommaToken(beforeToken)) {
344✔
335
    // If there is no comma, the element is the first element.
336
    return {
91✔
337
      comma: null,
338
      prevElement: null,
339
      last: beforeToken,
340
    };
341
  }
342
  const comma = beforeToken;
253✔
343
  const prevElement = sourceCode.getTokenBefore(beforeToken)!;
253✔
344

345
  if (isComma(prevElement)) {
253✔
346
    // If the previous element is empty,
347
    // the position of the comma is the end of the previous element's range.
348
    return {
1✔
349
      comma,
350
      prevElement: null,
351
      last: comma,
352
    };
353
  }
354

355
  const endInfo = getElementEndInfo(sourceCode, prevElement);
252✔
356

357
  return {
252✔
358
    comma: endInfo.comma,
359
    prevElement,
360
    last: endInfo.last,
361
  };
362
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc