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

ota-meshi / eslint-plugin-jsonc / 12941945364

24 Jan 2025 02:17AM UTC coverage: 72.146% (+0.1%) from 72.0%
12941945364

Pull #390

github

web-flow
Merge 60745b159 into 2dc3d182d
Pull Request #390: feat: improve auto-fix of sort rules

790 of 1299 branches covered (60.82%)

Branch coverage included in aggregate %.

81 of 95 new or added lines in 3 files covered. (85.26%)

1681 of 2126 relevant lines covered (79.07%)

124.26 hits per line

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

82.5
/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

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

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

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

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

58
  const toBeforeToken = to.node
170!
59
    ? sourceCode.getTokenBefore(getFirstTokenOfNode(sourceCode, to.node))!
60
    : to.before;
61
  let insertRange = toBeforeToken.range;
170✔
62
  const toBeforeNextToken = sourceCode.getTokenAfter(toBeforeToken, {
170✔
63
    includeComments: true,
64
  })!;
65
  if (toBeforeNextToken.loc!.start.line - toBeforeToken.loc.end.line > 1) {
170✔
66
    // If there are blank lines, the element is inserted after the blank lines.
67
    const offset = sourceCode.getIndexFromLoc({
8✔
68
      line: toBeforeNextToken.loc!.start.line - 1,
69
      column: 0,
70
    });
71
    insertRange = [offset, offset];
8✔
72
  }
73
  yield fixer.insertTextAfterRange(insertRange, targetInfo.insertCode);
170✔
74

75
  for (const removeRange of targetInfo.removeRanges) {
170✔
76
    yield fixer.removeRange(removeRange);
436✔
77
  }
78
}
79

80
/**
81
 * Calculate the range of the target information.
82
 */
83
function calcTargetInfo(
84
  sourceCode: SourceCode,
85
  target: Target,
86
): {
87
  insertCode: string;
88
  removeRanges: ESLintAST.Range[];
89
} {
90
  if (!target.node) {
170!
NEW
91
    return calcTargetInfoFromAround(sourceCode, target);
×
92
  }
93
  const node = target.node;
170✔
94
  const nodeLastToken = getLastTokenOfNode(sourceCode, node);
170✔
95

96
  const endInfo = getElementEndInfo(sourceCode, node);
170✔
97
  const prevInfo = getPrevElementInfo(sourceCode, node);
170✔
98

99
  let insertCode: string;
100

101
  const removeRanges: ESLintAST.Range[] = [];
170✔
102
  if (prevInfo.comma && prevInfo.end <= prevInfo.comma.range[0]) {
170✔
103
    insertCode = `${sourceCode.text.slice(
4✔
104
      prevInfo.end,
105
      prevInfo.comma.range[0],
106
    )}${sourceCode.text.slice(prevInfo.comma.range[1], nodeLastToken.range[1])}`;
107
    removeRanges.push(
4✔
108
      [prevInfo.end, prevInfo.comma.range[0]],
109
      [prevInfo.comma.range[1], nodeLastToken.range[1]],
110
    );
111
  } else {
112
    insertCode = sourceCode.text.slice(prevInfo.end, nodeLastToken.range[1]);
166✔
113
    removeRanges.push([prevInfo.end, nodeLastToken.range[1]]);
166✔
114
  }
115

116
  const hasTrailingComma =
117
    endInfo.comma && endInfo.comma.range[1] <= endInfo.end;
170✔
118
  if (!hasTrailingComma) {
170✔
119
    insertCode += ",";
92✔
120
    if (prevInfo.comma) {
92✔
121
      removeRanges.push(prevInfo.comma.range);
92✔
122
    }
123
  }
124
  insertCode += sourceCode.text.slice(nodeLastToken.range[1], endInfo.end);
170✔
125
  removeRanges.push([nodeLastToken.range[1], endInfo.end]);
170✔
126

127
  return {
170✔
128
    insertCode,
129
    removeRanges,
130
  };
131
}
132

133
/**
134
 * Calculate the range of the target information from the around tokens.
135
 */
136
function calcTargetInfoFromAround(
137
  sourceCode: SourceCode,
138
  target: AroundTarget,
139
): {
140
  insertCode: string;
141
  removeRanges: ESLintAST.Range[];
142
} {
NEW
143
  const hasTrailingComma = isComma(target.after);
×
NEW
144
  const codeStart = target.before.range[1]; // to include comments
×
145
  let codeEnd: number;
NEW
146
  if (hasTrailingComma) {
×
147
    // , /**/,
148
    //  ^^^^^^
NEW
149
    codeEnd = target.after.range[1];
×
150
  } else {
151
    // , /**/ ]
152
    //  ^^^^^^
NEW
153
    codeEnd = target.after.range[0];
×
154
  }
NEW
155
  let removeStart = codeStart;
×
NEW
156
  if (!hasTrailingComma) {
×
157
    // The target is always the second or subsequent element, so it always has a leading comma.
158
    // , /**/ ]
159
    // ^^^^^^^
NEW
160
    removeStart = target.before.range[0];
×
161
  }
162

NEW
163
  return {
×
164
    insertCode:
165
      sourceCode.text.slice(codeStart, codeEnd) + (hasTrailingComma ? "" : ","),
×
166
    removeRanges: [[removeStart, codeEnd]],
167
  };
168
}
169

170
/**
171
 * Get the first token of the node.
172
 */
173
function getFirstTokenOfNode(
174
  sourceCode: SourceCode,
175
  node: AST.JSONNode | ESLintAST.Token,
176
): ESLintAST.Token {
177
  let token = sourceCode.getFirstToken(node as never)!;
340✔
178
  let target: ESLintAST.Token | null = token;
340✔
179
  while (
340✔
180
    (target = sourceCode.getTokenBefore(target)) &&
684✔
181
    isOpeningParenToken(target)
182
  ) {
183
    token = target;
2✔
184
  }
185
  return token;
340✔
186
}
187

188
/**
189
 * Get the last token of the node.
190
 */
191
function getLastTokenOfNode(
192
  sourceCode: SourceCode,
193
  node: AST.JSONNode | ESLintAST.Token,
194
): ESLintAST.Token {
195
  let token = sourceCode.getLastToken(node as never)!;
510✔
196
  let target: ESLintAST.Token | null = token;
510✔
197
  while (
510✔
198
    (target = sourceCode.getTokenAfter(target)) &&
1,024✔
199
    isClosingParenToken(target)
200
  ) {
201
    token = target;
2✔
202
  }
203
  return token;
510✔
204
}
205

206
/**
207
 * Get the end of the target element and the next element and token information.
208
 */
209
function getElementEndInfo(
210
  sourceCode: SourceCode,
211
  node: AST.JSONNode | ESLintAST.Token,
212
): {
213
  // Trailing comma
214
  comma: ESLintAST.Token | null;
215
  // Next element token
216
  nextElement: ESLintAST.Token | null;
217
  // The end of the range of the target element
218
  end: number;
219
} {
220
  const lastToken = getLastTokenOfNode(sourceCode, node);
340✔
221
  const afterToken = sourceCode.getTokenAfter(lastToken)!;
340✔
222
  if (isNotCommaToken(afterToken)) {
340✔
223
    // If there is no comma, the element is the last element.
224
    return {
92✔
225
      comma: null,
226
      nextElement: null,
227
      end: calcEndWithTrailingComments(),
228
    };
229
  }
230
  const comma = afterToken;
248✔
231
  const nextElement = sourceCode.getTokenAfter(afterToken)!;
248✔
232
  if (isComma(nextElement)) {
248✔
233
    // If the next element is empty,
234
    // the position of the comma is the end of the element's range.
235
    return {
1✔
236
      comma,
237
      nextElement: null,
238
      end: comma.range[1],
239
    };
240
  }
241
  if (isClosingBrace(nextElement) || isClosingBracket(nextElement)) {
247✔
242
    // If the next token is a closing brace or bracket,
243
    // the position of the comma is the end of the element's range.
244
    return {
10✔
245
      comma,
246
      nextElement: null,
247
      end: calcEndWithTrailingComments(),
248
    };
249
  }
250

251
  if (node.loc.end.line === nextElement.loc.start.line) {
237✔
252
    // There is no line break between the target element and the next element.
253
    return {
144✔
254
      comma,
255
      nextElement,
256
      end: comma.range[1],
257
    };
258
  }
259
  // There are line breaks between the target element and the next element.
260
  if (
93✔
261
    node.loc.end.line < comma.loc.start.line &&
99✔
262
    comma.loc.end.line < nextElement.loc.start.line
263
  ) {
264
    // If there is a line break between the target element and a comma and the next element,
265
    // the position of the comma is the end of the element's range.
266
    return {
2✔
267
      comma,
268
      nextElement,
269
      end: comma.range[1],
270
    };
271
  }
272

273
  return {
91✔
274
    comma,
275
    nextElement,
276
    end: calcEndWithTrailingComments(),
277
  };
278

279
  /**
280
   * Calculate the end of the target element with trailing comments.
281
   */
282
  function calcEndWithTrailingComments() {
283
    let end = lastToken.range[1];
193✔
284
    let after = sourceCode.getTokenAfter(lastToken, {
193✔
285
      includeComments: true,
286
    })!;
287
    while (
193✔
288
      (isCommentToken(after) || isComma(after)) &&
719✔
289
      node.loc.end.line === after.loc!.end.line
290
    ) {
291
      end = after.range![1];
117✔
292
      after = sourceCode.getTokenAfter(after, {
117✔
293
        includeComments: true,
294
      })!;
295
    }
296
    return end;
193✔
297
  }
298
}
299

300
/**
301
 * Get the previous element and token information.
302
 */
303
function getPrevElementInfo(
304
  sourceCode: SourceCode,
305
  node: AST.JSONNode,
306
): {
307
  // Leading comma
308
  comma: ESLintAST.Token | null;
309
  // Previous element token
310
  prevElement: ESLintAST.Token | null;
311
  // The end of the range of the target element
312
  end: number;
313
} {
314
  const firstToken = getFirstTokenOfNode(sourceCode, node);
170✔
315
  const beforeToken = sourceCode.getTokenBefore(firstToken)!;
170✔
316
  if (isNotCommaToken(beforeToken)) {
170!
317
    // If there is no comma, the element is the first element.
NEW
318
    return {
×
319
      comma: null,
320
      prevElement: null,
321
      end: beforeToken.range[1],
322
    };
323
  }
324
  const comma = beforeToken;
170✔
325
  const prevElement = sourceCode.getTokenBefore(beforeToken)!;
170✔
326

327
  if (isComma(prevElement)) {
170!
328
    // If the previous element is empty,
329
    // the position of the comma is the end of the previous element's range.
NEW
330
    return {
×
331
      comma,
332
      prevElement: null,
333
      end: comma.range[1],
334
    };
335
  }
336

337
  const endInfo = getElementEndInfo(sourceCode, prevElement);
170✔
338

339
  return {
170✔
340
    comma: endInfo.comma,
341
    prevElement,
342
    end: endInfo.end,
343
  };
344
}
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