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

ota-meshi / eslint-plugin-jsonc / 15085399755

17 May 2025 12:54PM UTC coverage: 68.782% (-3.7%) from 72.499%
15085399755

push

github

web-flow
fix(deps): update dependency synckit to `^0.6.2 || ^0.7.3 || ^0.11.5` (#404)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: JounQin <admin@1stg.me>

1164 of 1985 branches covered (58.64%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

564 existing lines in 40 files now uncovered.

2011 of 2631 relevant lines covered (76.43%)

102.3 hits per line

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

87.78
/lib/rules/sort-array-values.ts
1
import naturalCompare from "natural-compare";
2
import { createRule } from "../utils";
1✔
3
import type { AST } from "jsonc-eslint-parser";
4
import { getStaticJSONValue } from "jsonc-eslint-parser";
5
import type { SourceCode } from "eslint";
1✔
6
import type { AroundTarget } from "../utils/fix-sort-elements";
7
import { fixForSorting } from "../utils/fix-sort-elements";
8

9
type JSONValue = ReturnType<typeof getStaticJSONValue>;
10

11
//------------------------------------------------------------------------------
3✔
12
// Helpers
13
//------------------------------------------------------------------------------
14

1✔
15
type UserOptions = PatternOption[];
1✔
16
type OrderTypeOption = "asc" | "desc";
1✔
17
type PatternOption = {
1✔
18
  pathPattern: string;
19
  order:
1!
20
    | OrderObject
21
    | (
22
        | string
23
        | {
1✔
24
            valuePattern?: string;
25
            order?: OrderObject;
69!
26
          }
69✔
27
      )[];
UNCOV
28
  minValues?: number;
×
UNCOV
29
};
×
30
type OrderObject = {
31
  type?: OrderTypeOption;
32
  caseSensitive?: boolean;
33
  natural?: boolean;
34
};
35
type ParsedOption = {
138✔
36
  isTargetArray: (node: JSONArrayData) => boolean;
10✔
37
  ignore: (data: JSONElementData) => boolean;
38
  isValidOrder: Validator;
128✔
39
  orderText: (data: JSONElementData) => string;
128!
40
};
128✔
41
type Validator = (a: JSONElementData, b: JSONElementData) => boolean;
42

43
type JSONElement = AST.JSONArrayExpression["elements"][number];
44
class JSONElementData {
45
  public readonly array: JSONArrayData;
UNCOV
46

×
UNCOV
47
  public readonly node: JSONElement;
×
UNCOV
48

×
49
  public readonly index: number;
50

51
  private cached: { value: JSONValue } | null = null;
52

53
  private cachedAround: AroundTarget | null = null;
54

55
  public get reportLoc() {
3,318✔
56
    if (this.node) {
362✔
57
      return this.node.loc;
58
    }
59
    const around = this.around;
60
    return {
364✔
61
      start: around.before.loc.end,
364✔
62
      end: around.after.loc.start,
364✔
63
    };
364✔
64
  }
364✔
65

66
  public get around(): AroundTarget {
67
    if (this.cachedAround) {
1✔
68
      return this.cachedAround;
69
    }
70
    const sourceCode = this.array.sourceCode;
434✔
71
    if (this.node) {
72
      return (this.cachedAround = {
73
        node: this.node,
94✔
74
        before: sourceCode.getTokenBefore(this.node as never)!,
94✔
75
        after: sourceCode.getTokenAfter(this.node as never)!,
94✔
76
      });
77
    }
78
    const before =
79
      this.index > 0
80
        ? this.array.elements[this.index - 1].around.after
81
        : sourceCode.getFirstToken(this.array.node as never)!;
209✔
82
    const after = sourceCode.getTokenAfter(before)!;
74✔
83
    return (this.cachedAround = { before, after });
74✔
84
  }
18✔
85

86
  public constructor(array: JSONArrayData, node: JSONElement, index: number) {
74✔
87
    this.array = array;
6✔
88
    this.node = node;
22✔
89
    this.index = index;
90
  }
91

92
  public get value() {
93
    return (
74✔
94
      this.cached ??
10✔
95
      (this.cached = {
32✔
96
        value: this.node == null ? null : getStaticJSONValue(this.node),
10✔
97
      })
10✔
98
    ).value;
99
  }
74✔
100
}
251✔
101
class JSONArrayData {
210✔
102
  public readonly node: AST.JSONArrayExpression;
103

104
  public readonly sourceCode: SourceCode;
105

106
  private cachedElements: JSONElementData[] | null = null;
41✔
107

41✔
108
  public constructor(node: AST.JSONArrayExpression, sourceCode: SourceCode) {
17✔
109
    this.node = node;
110
    this.sourceCode = sourceCode;
111
  }
112

113
  public get elements() {
114
    return (this.cachedElements ??= this.node.elements.map(
24✔
115
      (e, index) => new JSONElementData(this, e, index),
116
    ));
117
  }
118
}
119

120
/**
90✔
121
 * Build function which check that the given 2 names are in specific order.
90✔
122
 */
90✔
123
function buildValidatorFromType(
124
  order: OrderTypeOption,
90!
125
  insensitive: boolean,
90✔
126
  natural: boolean,
127
): Validator {
56!
128
  type Compare<T> = ([a, b]: T[]) => boolean;
56✔
129

56✔
130
  let compareValue: Compare<any> = ([a, b]) => a <= b;
56✔
131
  let compareText: Compare<string> = compareValue;
132

456✔
133
  if (natural) {
134
    compareText = ([a, b]) => naturalCompare(a, b) <= 0;
135
  }
35✔
136
  if (insensitive) {
30✔
137
    const baseCompareText = compareText;
138
    compareText = ([a, b]: string[]) =>
5✔
139
      baseCompareText([a.toLowerCase(), b.toLowerCase()]);
140
  }
141
  if (order === "desc") {
142
    const baseCompareText = compareText;
34✔
143
    compareText = (args: string[]) => baseCompareText(args.reverse());
34✔
144
    const baseCompareValue = compareValue;
120✔
145
    compareValue = (args) => baseCompareValue(args.reverse());
102✔
146
  }
2,000✔
UNCOV
147
  return (a: JSONElementData, b: JSONElementData) => {
×
148
    if (typeof a.value === "string" && typeof b.value === "string") {
149
      return compareText([a.value, b.value]);
150
    }
18!
151
    const type = getJSONPrimitiveType(a.value);
152
    if (type && type === getJSONPrimitiveType(b.value)) {
18!
153
      return compareValue([a.value, b.value]);
154
    }
18!
155
    // Unknown
18✔
156
    return true;
18✔
157
  };
18✔
158
}
222!
159

160
/**
161
 * Parse options
162
 */
163
function parseOptions(options: UserOptions): ParsedOption[] {
34✔
164
  return options.map((opt) => {
165
    const order = opt.order;
1,242✔
166
    const pathPattern = new RegExp(opt.pathPattern);
167
    const minValues: number = opt.minValues ?? 2;
200✔
168
    if (!Array.isArray(order)) {
490✔
169
      const type: OrderTypeOption = order.type ?? "asc";
490✔
170
      const insensitive = order.caseSensitive === false;
490✔
171
      const natural = Boolean(order.natural);
438✔
172

98✔
173
      return {
174
        isTargetArray,
340✔
175
        ignore: () => false,
50✔
176
        isValidOrder: buildValidatorFromType(type, insensitive, natural),
177
        orderText(data) {
290✔
178
          if (typeof data.value === "string") {
179
            return `${natural ? "natural " : ""}${
52✔
180
              insensitive ? "insensitive " : ""
UNCOV
181
            }${type}ending`;
×
182
          }
183
          return `${type}ending`;
34✔
184
        },
185
      };
186
    }
187
    const parsedOrder: {
188
      test: (v: JSONElementData) => boolean;
94✔
189
      isValidNestOrder: Validator;
4✔
190
    }[] = [];
191
    for (const o of order) {
192
      if (typeof o === "string") {
90✔
193
        parsedOrder.push({
90✔
194
          test: (v) => v.value === o,
90✔
195
          isValidNestOrder: () => true,
90✔
196
        });
336✔
197
      } else {
78✔
198
        const valuePattern = o.valuePattern ? new RegExp(o.valuePattern) : null;
78✔
199
        const nestOrder = o.order ?? {};
76✔
200
        const type: OrderTypeOption = nestOrder.type ?? "asc";
201
        const insensitive = nestOrder.caseSensitive === false;
2✔
202
        const natural = Boolean(nestOrder.natural);
203
        parsedOrder.push({
258!
UNCOV
204
          test: (v) =>
×
UNCOV
205
            valuePattern
×
206
              ? Boolean(getJSONPrimitiveType(v.value)) &&
207
                valuePattern.test(String(v.value))
336✔
208
              : true,
336✔
209
          isValidNestOrder: buildValidatorFromType(type, insensitive, natural),
210
        });
90✔
211
      }
76✔
212
    }
213

90✔
214
    return {
215
      isTargetArray,
216
      ignore: (v) => parsedOrder.every((p) => !p.test(v)),
217
      isValidOrder(a, b) {
218
        for (const p of parsedOrder) {
219
          const matchA = p.test(a);
78✔
220
          const matchB = p.test(b);
78!
UNCOV
221
          if (!matchA || !matchB) {
×
222
            if (matchA) {
223
              return true;
78✔
224
            }
225
            if (matchB) {
226
              return false;
227
            }
228
            continue;
229
          }
216✔
230
          return p.isValidNestOrder(a, b);
216✔
231
        }
200✔
232
        return false;
233
      },
16✔
234
      orderText: () => "specified",
12✔
235
    };
236

4!
UNCOV
237
    /**
×
238
     * Checks whether given node data is verify target
239
     */
4!
UNCOV
240
    function isTargetArray(data: JSONArrayData) {
×
241
      if (data.node.elements.length < minValues) {
242
        return false;
4✔
243
      }
244

1✔
245
      // Check whether the path is match or not.
246
      let path = "";
247
      let curr: AST.JSONNode = data.node;
248
      let p: AST.JSONNode | null = curr.parent;
1✔
249
      while (p) {
250
        if (p.type === "JSONProperty") {
251
          const name = getPropertyName(p);
252
          if (/^[$a-z_][\w$]*$/iu.test(name)) {
253
            path = `.${name}${path}`;
254
          } else {
255
            path = `[${JSON.stringify(name)}]${path}`;
256
          }
257
        } else if (p.type === "JSONArrayExpression") {
258
          const index = p.elements.indexOf(curr as never);
259
          path = `[${index}]${path}`;
260
        }
261
        curr = p;
262
        p = curr.parent;
263
      }
1✔
264
      if (path.startsWith(".")) {
265
        path = path.slice(1);
266
      }
267
      return pathPattern.test(path);
268
    }
269
  });
270

271
  /**
272
   * Gets the property name of the given `Property` node.
273
   */
274
  function getPropertyName(node: AST.JSONProperty): string {
275
    const prop = node.key;
276
    if (prop.type === "JSONIdentifier") {
277
      return prop.name;
278
    }
279
    return String(getStaticJSONValue(prop));
280
  }
281
}
282

283
/**
284
 * Get the type name from given value when value is primitive like value
285
 */
286
function getJSONPrimitiveType(val: JSONValue) {
287
  const t = typeof val;
288
  if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
289
    return t;
290
  }
291
  if (val === null) {
292
    return "null";
293
  }
294
  if (val === undefined) {
295
    return "undefined";
296
  }
297
  if (val instanceof RegExp) {
298
    return "regexp";
299
  }
300
  return null;
301
}
302

303
const ALLOW_ORDER_TYPES: OrderTypeOption[] = ["asc", "desc"];
304
const ORDER_OBJECT_SCHEMA = {
305
  type: "object",
306
  properties: {
307
    type: {
308
      enum: ALLOW_ORDER_TYPES,
309
    },
310
    caseSensitive: {
311
      type: "boolean",
312
    },
313
    natural: {
314
      type: "boolean",
315
    },
316
  },
317
  additionalProperties: false,
318
} as const;
319

320
//------------------------------------------------------------------------------
321
// Rule Definition
322
//------------------------------------------------------------------------------
323

324
export default createRule<UserOptions>("sort-array-values", {
325
  meta: {
90✔
326
    docs: {
90!
UNCOV
327
      description: "require array values to be sorted",
×
328
      recommended: null,
329
      extensionRule: false,
330
      layout: false,
90✔
331
    },
332
    fixable: "code",
333
    schema: {
334
      type: "array",
364✔
335
      items: {
18✔
336
        type: "object",
337
        properties: {
598✔
338
          pathPattern: { type: "string" },
346✔
339
          order: {
88✔
340
            oneOf: [
341
              {
258✔
342
                type: "array",
258✔
343
                items: {
69✔
344
                  anyOf: [
69✔
345
                    { type: "string" },
346
                    {
347
                      type: "object",
348
                      properties: {
349
                        valuePattern: {
350
                          type: "string",
351
                        },
352
                        order: ORDER_OBJECT_SCHEMA,
353
                      },
69✔
354
                      additionalProperties: false,
69✔
355
                    },
141✔
356
                  ],
36✔
357
                },
358
                uniqueItems: true,
105✔
359
              },
360
              ORDER_OBJECT_SCHEMA,
361
            ],
69✔
362
          },
363
          minValues: {
364
            type: "integer",
365
            minimum: 2,
366
          },
367
        },
368
        required: ["pathPattern", "order"],
369
        additionalProperties: false,
138!
370
      },
138✔
371
      minItems: 1,
UNCOV
372
    },
×
373

374
    messages: {
90✔
375
      sortValues:
376
        "Expected array values to be in {{orderText}} order. '{{thisValue}}' should be before '{{prevValue}}'.",
94✔
377
    },
94✔
378
    type: "suggestion",
94✔
379
  },
6✔
380
  create(context) {
381
    const sourceCode = context.sourceCode;
88✔
382
    if (!sourceCode.parserServices.isJSON) {
364✔
383
      return {};
384
    }
385
    // Parse options.
386
    const parsedOptions = parseOptions(context.options);
387

388
    /**
389
     * Verify for array element
390
     */
391
    function verifyArrayElement(data: JSONElementData, option: ParsedOption) {
392
      if (option.ignore(data)) {
393
        return;
394
      }
395
      const prevList = data.array.elements
396
        .slice(0, data.index)
397
        .reverse()
398
        .filter((d) => !option.ignore(d));
399

400
      if (prevList.length === 0) {
401
        return;
402
      }
403
      const prev = prevList[0];
404
      if (!option.isValidOrder(prev, data)) {
405
        const reportLoc = data.reportLoc;
406
        context.report({
407
          loc: reportLoc,
408
          messageId: "sortValues",
409
          data: {
410
            thisValue: toText(data),
411
            prevValue: toText(prev),
412
            orderText: option.orderText(data),
413
          },
414
          fix(fixer) {
415
            let moveTarget = prevList[0];
416
            for (const prev of prevList) {
417
              if (option.isValidOrder(prev, data)) {
418
                break;
419
              } else {
420
                moveTarget = prev;
421
              }
422
            }
423
            return fixForSorting(
424
              fixer,
425
              sourceCode,
426
              data.around,
427
              moveTarget.around,
428
            );
429
          },
430
        });
431
      }
432
    }
433

434
    /**
435
     * Convert to display text.
436
     */
437
    function toText(data: JSONElementData) {
438
      if (getJSONPrimitiveType(data.value)) {
439
        return String(data.value);
440
      }
441
      return sourceCode.getText(data.node! as never);
442
    }
443

444
    return {
445
      JSONArrayExpression(node) {
446
        const data = new JSONArrayData(node, sourceCode);
447
        const option = parsedOptions.find((o) => o.isTargetArray(data));
448
        if (!option) {
449
          return;
450
        }
451
        for (const element of data.elements) {
452
          verifyArrayElement(element, option);
453
        }
454
      },
455
    };
456
  },
457
});
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