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

ota-meshi / eslint-plugin-jsonc / 12947103330

24 Jan 2025 09:42AM UTC coverage: 72.523% (+0.3%) from 72.224%
12947103330

Pull #394

github

web-flow
Merge 466b59d95 into c59eaa077
Pull Request #394: minor refactor

800 of 1305 branches covered (61.3%)

Branch coverage included in aggregate %.

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

1681 of 2116 relevant lines covered (79.44%)

126.58 hits per line

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

93.97
/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
  ) {
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 fix information of the target.
92
 */
93
function calcTargetInfo(
94
  sourceCode: SourceCode,
95
  target: Target,
96
): {
97
  insertCode: string;
98
  removeRanges: ESLintAST.Range[];
99
} {
100
  const nodeEndIndex = target.node
172!
101
    ? getLastTokenOfNode(sourceCode, target.node).range[1]
102
    : target.after.range[0];
103

104
  const endInfo = getElementEndInfo(sourceCode, target);
172✔
105
  const prevInfo = getPrevElementInfo(sourceCode, target);
172✔
106

107
  let insertCode: string;
108

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

124
  const hasTrailingComma =
125
    endInfo.comma && endInfo.comma.range[1] <= endInfo.last.range![1];
172✔
126
  if (!hasTrailingComma) {
172✔
127
    insertCode += ",";
94✔
128
    if (prevInfo.comma) {
94✔
129
      removeRanges.push(prevInfo.comma.range);
94✔
130
    }
131
  }
132
  insertCode += sourceCode.text.slice(nodeEndIndex, endInfo.last.range![1]);
172✔
133
  removeRanges.push([nodeEndIndex, endInfo.last.range![1]]);
172✔
134

135
  return {
172✔
136
    insertCode,
137
    removeRanges,
138
  };
139
}
140

141
/**
142
 * Get the first token of the node.
143
 */
144
function getFirstTokenOfNode(
145
  sourceCode: SourceCode,
146
  node: AST.JSONNode | ESLintAST.Token,
147
): ESLintAST.Token {
148
  let token = sourceCode.getFirstToken(node as never)!;
344✔
149
  let target: ESLintAST.Token | null = token;
344✔
150
  while (
344✔
151
    (target = sourceCode.getTokenBefore(token)) &&
692✔
152
    isOpeningParenToken(target)
153
  ) {
154
    token = target;
2✔
155
  }
156
  return token;
344✔
157
}
158

159
/**
160
 * Get the last token of the node.
161
 */
162
function getLastTokenOfNode(
163
  sourceCode: SourceCode,
164
  node: AST.JSONNode | ESLintAST.Token,
165
): ESLintAST.Token {
166
  let token = sourceCode.getLastToken(node as never)!;
819✔
167
  let target: ESLintAST.Token | null = token;
819✔
168
  while (
819✔
169
    (target = sourceCode.getTokenAfter(token)) &&
1,644✔
170
    isClosingParenToken(target)
171
  ) {
172
    token = target;
3✔
173
  }
174
  return token;
819✔
175
}
176

177
/**
178
 * Get the end of the target element and the next element and token information.
179
 */
180
function getElementEndInfo(
181
  sourceCode: SourceCode,
182
  target: Target | { node: ESLintAST.Token },
183
): {
184
  // Trailing comma
185
  comma: ESLintAST.Token | null;
186
  // Next element token
187
  nextElement: ESLintAST.Token | null;
188
  // The last token of the target element
189
  last: ESLintAST.Token | ESTree.Comment;
190
} {
191
  const afterToken = target.node
424!
192
    ? sourceCode.getTokenAfter(getLastTokenOfNode(sourceCode, target.node))!
193
    : target.after;
194
  if (isNotCommaToken(afterToken)) {
424✔
195
    // If there is no comma, the element is the last element.
196
    return {
94✔
197
      comma: null,
198
      nextElement: null,
199
      last: getLastTokenWithTrailingComments(sourceCode, target),
200
    };
201
  }
202
  const comma = afterToken;
330✔
203
  const nextElement = sourceCode.getTokenAfter(afterToken)!;
330✔
204
  if (isComma(nextElement)) {
330✔
205
    // If the next element is empty,
206
    // the position of the comma is the end of the element's range.
207
    return {
1✔
208
      comma,
209
      nextElement: null,
210
      last: comma,
211
    };
212
  }
213
  if (isClosingBrace(nextElement) || isClosingBracket(nextElement)) {
329✔
214
    // If the next token is a closing brace or bracket,
215
    // the position of the comma is the end of the element's range.
216
    return {
10✔
217
      comma,
218
      nextElement: null,
219
      last: getLastTokenWithTrailingComments(sourceCode, target),
220
    };
221
  }
222

223
  const node = target.node;
319✔
224

225
  if (node && node.loc.end.line === nextElement.loc.start.line) {
319✔
226
    // There is no line break between the target element and the next element.
227
    return {
198✔
228
      comma,
229
      nextElement,
230
      last: comma,
231
    };
232
  }
233
  // There are line breaks between the target element and the next element.
234
  if (
121✔
235
    node &&
248✔
236
    node.loc.end.line < comma.loc.start.line &&
237
    comma.loc.end.line < nextElement.loc.start.line
238
  ) {
239
    // If there is a line break between the target element and a comma and the next element,
240
    // the position of the comma is the end of the element's range.
241
    return {
2✔
242
      comma,
243
      nextElement,
244
      last: comma,
245
    };
246
  }
247

248
  return {
119✔
249
    comma,
250
    nextElement,
251
    last: getLastTokenWithTrailingComments(sourceCode, target),
252
  };
253
}
254

255
/**
256
 * Get the last token of the target element with trailing comments.
257
 */
258
function getLastTokenWithTrailingComments(
259
  sourceCode: SourceCode,
260
  target: Target | { node: ESLintAST.Token },
261
) {
262
  if (!target.node) {
223!
NEW
263
    return sourceCode.getTokenBefore(target.after, {
×
264
      includeComments: true,
265
    })!;
266
  }
267
  const node = target.node;
223✔
268
  let last: ESLintAST.Token | ESTree.Comment = getLastTokenOfNode(
223✔
269
    sourceCode,
270
    node,
271
  );
272
  let after: ESLintAST.Token | ESTree.Comment | null;
273
  while (
223✔
274
    (after = sourceCode.getTokenAfter(last, {
1,249✔
275
      includeComments: true,
276
    })) &&
277
    (isCommentToken(after) || isComma(after)) &&
278
    node.loc.end.line === after.loc!.end.line
279
  ) {
280
    last = after;
151✔
281
  }
282
  return last;
223✔
283
}
284

285
/**
286
 * Get the previous element and token information.
287
 */
288
function getPrevElementInfo(
289
  sourceCode: SourceCode,
290
  target: Target,
291
): {
292
  // Leading comma
293
  comma: ESLintAST.Token | null;
294
  // Previous element token
295
  prevElement: ESLintAST.Token | null;
296
  // The last token of the target element
297
  last: ESLintAST.Token | ESTree.Comment;
298
} {
299
  const beforeToken = target.node
344!
300
    ? sourceCode.getTokenBefore(getFirstTokenOfNode(sourceCode, target.node))!
301
    : target.before;
302
  if (isNotCommaToken(beforeToken)) {
344✔
303
    // If there is no comma, the element is the first element.
304
    return {
91✔
305
      comma: null,
306
      prevElement: null,
307
      last: beforeToken,
308
    };
309
  }
310
  const comma = beforeToken;
253✔
311
  const prevElement = sourceCode.getTokenBefore(beforeToken)!;
253✔
312

313
  if (isComma(prevElement)) {
253✔
314
    // If the previous element is empty,
315
    // the position of the comma is the end of the previous element's range.
316
    return {
1✔
317
      comma,
318
      prevElement: null,
319
      last: comma,
320
    };
321
  }
322

323
  const endInfo = getElementEndInfo(sourceCode, { node: prevElement });
252✔
324

325
  return {
252✔
326
    comma: endInfo.comma,
327
    prevElement,
328
    last: endInfo.last,
329
  };
330
}
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