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

ota-meshi / eslint-plugin-jsonc / 28832408303

07 Jul 2026 12:19AM UTC coverage: 70.878% (+1.1%) from 69.766%
28832408303

push

github

web-flow
feat(sort-array-values): add `key` option to sort object arrays by property (#519)

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

1064 of 1307 branches covered (81.41%)

Branch coverage included in aggregate %.

57 of 109 new or added lines in 1 file covered. (52.29%)

5 existing lines in 1 file now uncovered.

7520 of 10804 relevant lines covered (69.6%)

93.36 hits per line

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

74.35
/lib/rules/sort-array-values.ts
1
import naturalCompare from "natural-compare";
✔
2
import { createRule } from "../utils/index.ts";
×
3
import type { AST } from "jsonc-eslint-parser";
×
4
import { getStaticJSONValue } from "jsonc-eslint-parser";
×
5
import type { AroundTarget } from "../utils/fix-sort-elements.ts";
×
6
import {
×
7
  fixToMoveDownForSorting,
×
8
  fixToMoveUpForSorting,
×
9
} from "../utils/fix-sort-elements.ts";
1✔
10
import { calcShortestEditScript } from "../utils/calc-shortest-edit-script.ts";
1✔
11
import type { JSONCSourceCode } from "../language/jsonc-source-code.ts";
1✔
12

1✔
13
type JSONValue = ReturnType<typeof getStaticJSONValue>;
1✔
14

1✔
15
//------------------------------------------------------------------------------
1✔
16
// Helpers
1✔
17
//------------------------------------------------------------------------------
1✔
18

1✔
19
type UserOptions = PatternOption[];
1✔
20
type OrderTypeOption = "asc" | "desc";
1✔
21
type PatternOption = {
1✔
22
  pathPattern: string;
1✔
23
  order:
1✔
24
    | OrderObject
1✔
25
    | (
1✔
26
        | string
1✔
27
        | {
1✔
28
            valuePattern?: string;
1✔
29
            order?: OrderObject;
1✔
30
          }
1✔
31
      )[];
1✔
32
  minValues?: number;
1✔
33
};
1✔
34
type OrderObject = {
1✔
35
  type?: OrderTypeOption;
1✔
36
  caseSensitive?: boolean;
1✔
37
  natural?: boolean;
1✔
38
  key?: string;
1✔
39
};
1✔
40
type ParsedOption = {
1✔
41
  isTargetArray: (node: JSONArrayData) => boolean;
1✔
42
  ignore: (data: JSONElementData) => boolean;
1✔
43
  isValidOrder: Validator;
1✔
44
  orderText: (data: JSONElementData) => string;
1✔
45
  key?: string;
1✔
46
};
1✔
47
type Validator = (a: JSONElementData, b: JSONElementData) => boolean;
1✔
48

1✔
49
type JSONElement = AST.JSONArrayExpression["elements"][number];
1✔
50
class JSONElementData {
1✔
51
  public readonly array: JSONArrayData;
409✔
52

409✔
53
  public readonly node: JSONElement;
409✔
54

409✔
55
  public readonly index: number;
409✔
56

409✔
57
  private cached: { value: JSONValue } | null = null;
409✔
58

409✔
59
  private cachedKeyValues: Map<string, JSONValue | undefined> | null = null;
409✔
60

409✔
61
  private cachedAround: AroundTarget | null = null;
409✔
62

409✔
63
  public get reportLoc() {
409✔
64
    if (this.node) {
77✔
65
      return this.node.loc;
77✔
66
    }
77✔
67
    const around = this.around;
77!
68
    return {
×
69
      start: around.before.loc.end,
×
70
      end: around.after.loc.start,
×
71
    };
×
UNCOV
72
  }
×
UNCOV
73

×
UNCOV
74
  public get around(): AroundTarget {
✔
75
    if (this.cachedAround) {
154✔
76
      return this.cachedAround;
16✔
77
    }
16✔
78
    const sourceCode = this.array.sourceCode;
154✔
79
    if (this.node) {
138✔
80
      return (this.cachedAround = {
138✔
81
        node: this.node,
138✔
82
        before: sourceCode.getTokenBefore(this.node)!,
138✔
83
        after: sourceCode.getTokenAfter(this.node)!,
138✔
84
      });
×
85
    }
×
86
    const before =
×
87
      this.index > 0
×
88
        ? this.array.elements[this.index - 1].around.after
×
89
        : sourceCode.getFirstToken(this.array.node);
×
90
    const after = sourceCode.getTokenAfter(before)!;
×
91
    return (this.cachedAround = { before, after });
×
92
  }
×
93

×
94
  public constructor(array: JSONArrayData, node: JSONElement, index: number) {
✔
95
    this.array = array;
408✔
96
    this.node = node;
408✔
97
    this.index = index;
408✔
98
  }
408✔
99

×
100
  public get value() {
✔
101
    return (
2,671✔
102
      this.cached ??
2,671✔
103
      (this.cached = {
378✔
104
        value: this.node == null ? null : getStaticJSONValue(this.node),
378✔
105
      })
378✔
106
    ).value;
2,671✔
107
  }
2,671✔
NEW
108

×
NEW
109
  /**
×
NEW
110
   * Get the value of the given property key when this element is an object.
×
NEW
111
   * Only the matched property's value is evaluated (and memoized per key),
×
NEW
112
   * avoiding a full deep conversion of the whole element. Returns `undefined`
×
NEW
113
   * for non-object elements or when the key is absent.
×
NEW
114
   */
×
NEW
115
  public getValueForKey(key: string): JSONValue | undefined {
✔
116
    const cache = (this.cachedKeyValues ??= new Map());
156✔
117
    if (cache.has(key)) {
156✔
118
      return cache.get(key);
112✔
119
    }
112✔
120
    let result: JSONValue | undefined;
156✔
121
    const node = this.node;
44✔
122
    if (node && node.type === "JSONObjectExpression") {
156✔
123
      for (const prop of node.properties) {
40✔
124
        if (getPropertyName(prop) === key) {
40✔
125
          result = getStaticJSONValue(prop.value);
38✔
NEW
126
          break;
×
NEW
127
        }
×
NEW
128
      }
×
NEW
129
    }
×
NEW
130
    cache.set(key, result);
✔
NEW
131
    return result;
×
NEW
132
  }
×
133
}
×
134
class JSONArrayData {
✔
135
  public readonly node: AST.JSONArrayExpression;
111✔
136

111✔
137
  public readonly sourceCode: JSONCSourceCode;
111✔
138

111✔
139
  private cachedElements: JSONElementData[] | null = null;
111✔
140

111✔
141
  public constructor(
111✔
142
    node: AST.JSONArrayExpression,
110✔
143
    sourceCode: JSONCSourceCode,
110✔
144
  ) {
110✔
145
    this.node = node;
110✔
146
    this.sourceCode = sourceCode;
110✔
147
  }
110✔
148

111✔
149
  public get elements() {
111✔
150
    return (this.cachedElements ??= this.node.elements.map(
104✔
151
      (e, index) => new JSONElementData(this, e, index),
104✔
152
    ));
104✔
153
  }
104✔
154
}
111✔
155

1✔
156
/**
1✔
157
 * Build function which check that the given 2 names are in specific order.
1✔
158
 */
1✔
159
function buildValidatorFromType(
92✔
160
  order: OrderTypeOption,
92✔
161
  insensitive: boolean,
92✔
162
  natural: boolean,
92✔
163
  key?: string,
92✔
164
): Validator {
92✔
165
  type Compare<T> = ([a, b]: T[]) => boolean;
92✔
166

92✔
167
  let compareValue: Compare<any> = ([a, b]) => a <= b;
92✔
168
  let compareText: Compare<string> = compareValue;
92✔
169

92✔
170
  if (natural) {
92✔
171
    compareText = ([a, b]) => naturalCompare(a, b) <= 0;
4✔
172
  }
4✔
173
  if (insensitive) {
92✔
174
    const baseCompareText = compareText;
×
UNCOV
175
    compareText = ([a, b]: string[]) =>
✔
176
      baseCompareText([a.toLowerCase(), b.toLowerCase()]);
24✔
177
  }
×
178
  if (order === "desc") {
✔
179
    const baseCompareText = compareText;
×
180
    compareText = (args: string[]) => baseCompareText(args.reverse());
✔
181
    const baseCompareValue = compareValue;
×
182
    compareValue = (args) => baseCompareValue(args.reverse());
✔
183
  }
×
NEW
184

×
NEW
185
  /**
×
NEW
186
   * Compare two resolved values (an element value, or a value extracted by key).
×
NEW
187
   */
×
NEW
188
  function compare(aVal: JSONValue, bVal: JSONValue): boolean {
✔
189
    if (typeof aVal === "string" && typeof bVal === "string") {
359✔
190
      return compareText([aVal, bVal]);
316✔
191
    }
316✔
192
    const type = getJSONPrimitiveType(aVal);
359✔
193
    if (type && type === getJSONPrimitiveType(bVal)) {
359✔
194
      return compareValue([aVal, bVal]);
18✔
195
    }
18✔
196
    // Unknown
359✔
197
    return true;
359✔
198
  }
359✔
NEW
199

×
NEW
200
  return (a: JSONElementData, b: JSONElementData) => {
✔
201
    if (key) {
359✔
202
      const aVal = a.getValueForKey(key);
36✔
203
      const bVal = b.getValueForKey(key);
36✔
NEW
204
      // Elements missing the key are left in place — they are excluded from
×
NEW
205
      // ordering by the caller.
×
NEW
206
      if (aVal === undefined || bVal === undefined) return true;
×
NEW
207
      return compare(aVal, bVal);
×
NEW
208
    }
×
NEW
209

×
NEW
210
    return compare(a.value, b.value);
✔
211
  };
×
212
}
×
213

×
214
/**
×
215
 * Parse options
×
216
 */
×
217
function parseOptions(options: UserOptions): ParsedOption[] {
106✔
218
  return options.map((opt) => {
106✔
219
    const order = opt.order;
106✔
220
    const pathPattern = new RegExp(opt.pathPattern);
106✔
221
    const minValues: number = opt.minValues ?? 2;
106✔
222
    if (!Array.isArray(order)) {
106✔
223
      const type: OrderTypeOption = order.type ?? "asc";
64!
224
      const insensitive = order.caseSensitive === false;
64✔
225
      const natural = Boolean(order.natural);
64✔
226
      const key = order.key;
64✔
227

×
228
      return {
×
229
        isTargetArray,
×
NEW
230
        // When sorting by a key, elements missing that key are skipped —
×
NEW
231
        // excluded from the sort and left in place.
×
NEW
232
        ignore: key
✔
NEW
233
          ? (v) => v.getValueForKey(key) === undefined
✔
NEW
234
          : () => false,
✔
NEW
235
        isValidOrder: buildValidatorFromType(type, insensitive, natural, key),
×
UNCOV
236
        orderText(data) {
✔
237
          const base =
41✔
238
            typeof data.value === "string" || key
41✔
239
              ? `${natural ? "natural " : ""}${
36✔
240
                  insensitive ? "insensitive " : ""
36✔
241
                }${type}ending`
36✔
242
              : `${type}ending`;
5✔
NEW
243
          return key ? `${base} by '${key}'` : base;
✔
244
        },
×
NEW
245
        key,
×
246
      };
×
247
    }
×
248
    const parsedOrder: {
✔
249
      test: (v: JSONElementData) => boolean;
×
250
      isValidNestOrder: Validator;
×
251
    }[] = [];
×
252
    for (const o of order) {
✔
253
      if (typeof o === "string") {
✔
254
        parsedOrder.push({
×
255
          test: (v) => v.value === o,
✔
256
          isValidNestOrder: () => true,
×
257
        });
×
258
      } else {
✔
259
        const valuePattern = o.valuePattern ? new RegExp(o.valuePattern) : null;
28!
260
        const nestOrder = o.order ?? {};
28!
261
        const type: OrderTypeOption = nestOrder.type ?? "asc";
28!
262
        const insensitive = nestOrder.caseSensitive === false;
×
263
        const natural = Boolean(nestOrder.natural);
×
NEW
264
        const itemKey = nestOrder.key;
×
265
        parsedOrder.push({
×
NEW
266
          test: (v) => {
✔
267
            if (itemKey) {
294✔
268
              const keyVal = v.getValueForKey(itemKey);
48✔
269
              return valuePattern
48!
NEW
270
                ? keyVal !== undefined && valuePattern.test(String(keyVal))
×
271
                : keyVal !== undefined;
48✔
272
            }
48✔
273
            return valuePattern
294!
274
              ? Boolean(getJSONPrimitiveType(v.value)) &&
×
NEW
275
                  valuePattern.test(String(v.value))
✔
276
              : true;
246✔
277
          },
294✔
NEW
278
          isValidNestOrder: buildValidatorFromType(
×
NEW
279
            type,
×
NEW
280
            insensitive,
×
NEW
281
            natural,
×
NEW
282
            itemKey,
×
NEW
283
          ),
×
284
        });
×
285
      }
×
286
    }
×
287

×
288
    return {
✔
289
      isTargetArray,
×
290
      ignore: (v) => parsedOrder.every((p) => !p.test(v)),
✔
291
      isValidOrder(a, b) {
✔
292
        for (const p of parsedOrder) {
304✔
293
          const matchA = p.test(a);
750✔
294
          const matchB = p.test(b);
750✔
295
          if (!matchA || !matchB) {
750✔
296
            if (matchA) {
646✔
297
              return true;
170✔
298
            }
170✔
299
            if (matchB) {
646✔
300
              return false;
30✔
301
            }
30✔
302
            continue;
646✔
303
          }
446✔
304
          return p.isValidNestOrder(a, b);
750✔
305
        }
104✔
306
        return false;
304!
307
      },
304✔
308
      orderText: () => "specified",
✔
309
    };
×
310

×
311
    /**
×
312
     * Checks whether given node data is verify target
×
313
     */
×
314
    function isTargetArray(data: JSONArrayData) {
×
315
      if (data.node.elements.length < minValues) {
110✔
316
        return false;
4✔
317
      }
4✔
318

110✔
319
      // Check whether the path is match or not.
110✔
320
      let path = "";
110✔
321
      let curr: AST.JSONNode = data.node;
106✔
322
      let p: AST.JSONNode | null = curr.parent;
106✔
323
      while (p) {
110✔
324
        if (p.type === "JSONProperty") {
400✔
325
          const name = getPropertyName(p);
94✔
326
          if (/^[$a-z_][\w$]*$/iu.test(name)) {
94✔
327
            path = `.${name}${path}`;
92✔
328
          } else {
94✔
329
            path = `[${JSON.stringify(name)}]${path}`;
2✔
330
          }
2✔
331
        } else if (p.type === "JSONArrayExpression") {
400!
332
          const index = (p.elements as AST.JSONNode[]).indexOf(curr);
×
333
          path = `[${index}]${path}`;
×
334
        }
×
335
        curr = p;
400✔
336
        p = curr.parent;
400✔
337
      }
400✔
338
      if (path.startsWith(".")) {
110✔
339
        path = path.slice(1);
92✔
340
      }
92✔
341
      return pathPattern.test(path);
110✔
342
    }
110✔
343
  });
×
NEW
344
}
×
345

×
NEW
346
/**
×
NEW
347
 * Gets the property name of the given `Property` node.
×
NEW
348
 */
×
349
function getPropertyName(node: AST.JSONProperty): string {
134✔
350
  const prop = node.key;
134✔
351
  if (prop.type === "JSONIdentifier") {
134!
NEW
352
    return prop.name;
×
353
  }
×
354
  return String(getStaticJSONValue(prop));
134✔
355
}
134✔
356

×
357
/**
×
358
 * Get the type name from given value when value is primitive like value
×
359
 */
×
360
function getJSONPrimitiveType(val: JSONValue) {
224✔
361
  const t = typeof val;
224✔
362
  if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
224✔
363
    return t;
201✔
364
  }
201✔
365
  if (val === null) {
224✔
366
    return "null";
15✔
367
  }
15✔
368
  if (val === undefined) {
224!
369
    return "undefined";
×
370
  }
×
371
  if (val instanceof RegExp) {
224!
372
    return "regexp";
×
373
  }
×
374
  return null;
224✔
375
}
224✔
376

1✔
377
const ALLOW_ORDER_TYPES: OrderTypeOption[] = ["asc", "desc"];
1✔
378
const ORDER_OBJECT_SCHEMA = {
1✔
379
  type: "object",
1✔
380
  properties: {
1✔
381
    type: {
1✔
382
      enum: ALLOW_ORDER_TYPES,
1✔
383
    },
1✔
384
    caseSensitive: {
1✔
385
      type: "boolean",
1✔
386
    },
1✔
387
    natural: {
1✔
388
      type: "boolean",
1✔
389
    },
1✔
390
    key: {
1✔
391
      type: "string",
1✔
392
    },
1✔
393
  },
1✔
394
  additionalProperties: false,
1✔
395
} as const;
1✔
396

1✔
397
//------------------------------------------------------------------------------
1✔
398
// Rule Definition
1✔
399
//------------------------------------------------------------------------------
1✔
400

1✔
401
export default createRule<UserOptions>("sort-array-values", {
1✔
402
  meta: {
1✔
403
    docs: {
1✔
404
      description: "require array values to be sorted",
1✔
405
      recommended: null,
1✔
406
      extensionRule: false,
1✔
407
      layout: false,
1✔
408
    },
1✔
409
    fixable: "code",
1✔
410
    schema: {
1✔
411
      type: "array",
1✔
412
      items: {
1✔
413
        type: "object",
1✔
414
        properties: {
1✔
415
          pathPattern: { type: "string" },
1✔
416
          order: {
1✔
417
            oneOf: [
1✔
418
              {
1✔
419
                type: "array",
1✔
420
                items: {
1✔
421
                  anyOf: [
1✔
422
                    { type: "string" },
1✔
423
                    {
1✔
424
                      type: "object",
1✔
425
                      properties: {
1✔
426
                        valuePattern: {
1✔
427
                          type: "string",
1✔
428
                        },
1✔
429
                        order: ORDER_OBJECT_SCHEMA,
1✔
430
                      },
1✔
431
                      additionalProperties: false,
1✔
432
                    },
1✔
433
                  ],
1✔
434
                },
1✔
435
                uniqueItems: true,
1✔
436
              },
1✔
437
              ORDER_OBJECT_SCHEMA,
1✔
438
            ],
1✔
439
          },
1✔
440
          minValues: {
1✔
441
            type: "integer",
1✔
442
            minimum: 2,
1✔
443
          },
1✔
444
        },
1✔
445
        required: ["pathPattern", "order"],
1✔
446
        additionalProperties: false,
1✔
447
      },
1✔
448
      minItems: 1,
1✔
449
    },
1✔
450

1✔
451
    messages: {
1✔
452
      shouldBeBefore:
1✔
453
        "Expected array values to be in {{orderText}} order. '{{thisValue}}' should be before '{{targetValue}}'.",
1✔
454
      shouldBeAfter:
1✔
455
        "Expected array values to be in {{orderText}} order. '{{thisValue}}' should be after '{{targetValue}}'.",
1✔
456
    },
1✔
457
    type: "suggestion",
1✔
458
  },
1✔
459
  create(context) {
1✔
460
    const sourceCode = context.sourceCode;
106✔
461
    if (!sourceCode.parserServices.isJSON) {
106!
462
      return {};
×
463
    }
×
464
    // Parse options.
106✔
465
    const parsedOptions = parseOptions(context.options);
106✔
466

106✔
467
    /**
106✔
468
     * Sort elements by bubble sort.
106✔
469
     */
106✔
470
    function bubbleSort(elements: JSONElementData[], option: ParsedOption) {
106✔
471
      const l = elements.length;
104✔
472
      const result = [...elements];
104✔
473
      let swapped: boolean;
104✔
474
      do {
104✔
475
        swapped = false;
191✔
476
        for (let nextIndex = 1; nextIndex < l; nextIndex++) {
191✔
477
          const prevIndex = nextIndex - 1;
559✔
478
          if (option.isValidOrder(result[prevIndex], result[nextIndex]))
559✔
479
            continue;
559✔
480
          [result[prevIndex], result[nextIndex]] = [
559✔
481
            result[nextIndex],
117✔
482
            result[prevIndex],
117✔
483
          ];
117✔
484
          swapped = true;
117✔
485
        }
117✔
486
      } while (swapped);
104✔
487
      return result;
104✔
488
    }
104✔
489

106✔
490
    /**
106✔
491
     * Verify for array elements
106✔
492
     */
106✔
493
    function verifyArrayElements(
106✔
494
      elements: JSONElementData[],
104✔
495
      option: ParsedOption,
104✔
496
    ) {
104✔
497
      const sorted = bubbleSort(elements, option);
104✔
498
      const editScript = calcShortestEditScript(elements, sorted);
104✔
499
      for (let index = 0; index < editScript.length; index++) {
104✔
500
        const edit = editScript[index];
465✔
501
        if (edit.type !== "delete") continue;
465✔
502
        const insertEditIndex = editScript.findIndex(
465✔
503
          (e) => e.type === "insert" && e.b === edit.a,
77✔
504
        );
77✔
505
        if (insertEditIndex === -1) {
465!
506
          // should not happen
×
507
          continue;
×
508
        }
×
509
        if (index < insertEditIndex) {
465✔
510
          const target = findInsertAfterTarget(edit.a, insertEditIndex);
59✔
511
          if (!target) {
59!
512
            // should not happen
×
513
            continue;
×
514
          }
×
515
          context.report({
59✔
516
            loc: edit.a.reportLoc,
59✔
517
            messageId: "shouldBeAfter",
59✔
518
            data: {
59✔
519
              thisValue: toText(edit.a, option.key),
59✔
520
              targetValue: toText(target, option.key),
59✔
521
              orderText: option.orderText(edit.a),
59✔
522
            },
59✔
523
            *fix(fixer) {
59✔
524
              yield* fixToMoveDownForSorting(
59✔
525
                fixer,
59✔
526
                sourceCode,
59✔
527
                edit.a.around,
59✔
528
                target.around,
59✔
529
              );
59✔
530
            },
59✔
531
          });
59✔
532
        } else {
465✔
533
          const target = findInsertBeforeTarget(edit.a, insertEditIndex);
18✔
534
          if (!target) {
18!
535
            // should not happen
×
536
            continue;
×
537
          }
×
538
          context.report({
18✔
539
            loc: edit.a.reportLoc,
18✔
540
            messageId: "shouldBeBefore",
18✔
541
            data: {
18✔
542
              thisValue: toText(edit.a, option.key),
18✔
543
              targetValue: toText(target, option.key),
18✔
544
              orderText: option.orderText(edit.a),
18✔
545
            },
18✔
546
            *fix(fixer) {
18✔
547
              yield* fixToMoveUpForSorting(
18✔
548
                fixer,
18✔
549
                sourceCode,
18✔
550
                edit.a.around,
18✔
551
                target.around,
18✔
552
              );
18✔
553
            },
18✔
554
          });
18✔
555
        }
18✔
556
      }
465✔
557

465✔
558
      /**
465✔
559
       * Find insert after target
465✔
560
       */
465✔
561
      function findInsertAfterTarget(
465✔
562
        element: JSONElementData,
59✔
563
        insertEditIndex: number,
59✔
564
      ) {
59✔
565
        for (let index = insertEditIndex - 1; index >= 0; index--) {
59✔
566
          const edit = editScript[index];
75✔
567
          if (edit.type === "delete" && edit.a === element) break;
75!
568
          if (edit.type !== "common") continue;
75✔
569
          return edit.a;
75✔
570
        }
59✔
571

59✔
572
        let lastTarget: JSONElementData | null = null;
59!
573
        for (
×
574
          let index = elements.indexOf(element) + 1;
×
575
          index < elements.length;
×
576
          index++
×
577
        ) {
×
578
          const el = elements[index];
×
579
          if (option.isValidOrder(el, element)) {
×
580
            lastTarget = el;
×
581
            continue;
×
582
          }
×
583
          return lastTarget;
×
584
        }
×
585
        return lastTarget;
×
586
      }
59✔
587

465✔
588
      /**
465✔
589
       * Find insert before target
465✔
590
       */
465✔
591
      function findInsertBeforeTarget(
465✔
592
        element: JSONElementData,
18✔
593
        insertEditIndex: number,
18✔
594
      ) {
18✔
595
        for (
18✔
596
          let index = insertEditIndex + 1;
18✔
597
          index < editScript.length;
18✔
598
          index++
18✔
599
        ) {
18✔
600
          const edit = editScript[index];
18✔
601
          if (edit.type === "delete" && edit.a === element) break;
18!
602
          if (edit.type !== "common") continue;
18!
603
          return edit.a;
18✔
604
        }
18✔
605

18✔
606
        let lastTarget: JSONElementData | null = null;
18!
607
        for (let index = elements.indexOf(element) - 1; index >= 0; index--) {
×
608
          const el = elements[index];
×
609
          if (option.isValidOrder(element, el)) {
×
610
            lastTarget = el;
×
611
            continue;
×
612
          }
×
613
          return lastTarget;
×
614
        }
×
615
        return lastTarget;
×
616
      }
18✔
617
    }
104✔
618

106✔
619
    /**
106✔
620
     * Convert to display text.
106✔
621
     */
106✔
622
    function toText(data: JSONElementData, key?: string) {
106✔
623
      if (key) {
154✔
624
        const keyVal = data.getValueForKey(key);
12✔
625
        if (keyVal !== undefined) {
12✔
626
          return String(keyVal);
12✔
627
        }
12✔
628
      }
12✔
629
      if (getJSONPrimitiveType(data.value)) {
154✔
630
        return String(data.value);
138✔
631
      }
138✔
632
      return sourceCode.getText(data.node!);
154✔
633
    }
154✔
634

106✔
635
    return {
106✔
636
      JSONArrayExpression(node) {
106✔
637
        const data = new JSONArrayData(node, sourceCode);
110✔
638
        const option = parsedOptions.find((o) => o.isTargetArray(data));
110✔
639
        if (!option) {
110✔
640
          return;
6✔
641
        }
6✔
642
        verifyArrayElements(
110✔
643
          data.elements.filter((d) => !option.ignore(d)),
104✔
644
          option,
104✔
645
        );
104✔
646
      },
110✔
647
    };
106✔
648
  },
106✔
649
});
1✔
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