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

ota-meshi / eslint-plugin-yml / 18185565839

02 Oct 2025 06:46AM UTC coverage: 90.271% (-1.4%) from 91.662%
18185565839

Pull #482

github

web-flow
Merge c4f6eca06 into b332c3d5a
Pull Request #482: feat(sort-keys): improve to calculate the minimum edit distance for sorting and report the optimal sorting direction

1257 of 1463 branches covered (85.92%)

Branch coverage included in aggregate %.

232 of 272 new or added lines in 3 files covered. (85.29%)

10 existing lines in 2 files now uncovered.

2501 of 2700 relevant lines covered (92.63%)

680.92 hits per line

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

75.74
/src/rules/sort-sequence-values.ts
1
import type { RuleFixer, SourceCode, YAMLToken } from "../types";
6✔
2
import naturalCompare from "natural-compare";
1✔
3
import { createRule } from "../utils/index";
1✔
4
import { isComma } from "../utils/ast-utils";
1✔
5
import type { AST } from "yaml-eslint-parser";
6
import { getStaticYAMLValue } from "yaml-eslint-parser";
1✔
7
import { getSourceCode } from "../utils/compat";
1✔
8
import { calcShortestEditScript } from "src/utils/calc-shortest-edit-script";
1✔
9

10
type YAMLValue = ReturnType<typeof getStaticYAMLValue>;
11

12
//------------------------------------------------------------------------------
13
// Helpers
14
//------------------------------------------------------------------------------
15

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

44
type YAMLEntry = AST.YAMLSequence["entries"][number];
45
type AroundTokens = { before: YAMLToken; after: YAMLToken };
46
class YAMLEntryData {
47
  public readonly sequence: YAMLSequenceData;
48

49
  public readonly node: YAMLEntry;
50

51
  public readonly index: number;
52

53
  public readonly anchorAlias: {
54
    anchors: Set<string>;
55
    aliases: Set<string>;
56
  };
57

58
  private cached: { value: YAMLValue } | null = null;
598✔
59

60
  private cachedRange: [number, number] | null = null;
598✔
61

62
  private cachedAroundTokens: AroundTokens | null = null;
598✔
63

64
  public get reportLoc() {
65
    if (this.node) {
106✔
66
      return this.node.loc;
106✔
67
    }
68
    const aroundTokens = this.aroundTokens;
×
69
    return {
×
70
      start: aroundTokens.before.loc.end,
71
      end: aroundTokens.after.loc.start,
72
    };
73
  }
74

75
  public get range(): [number, number] {
76
    if (this.node) {
4✔
77
      return this.node.range;
4✔
78
    }
79
    if (this.cachedRange) {
×
80
      return this.cachedRange;
×
81
    }
82
    const aroundTokens = this.aroundTokens;
×
83
    return (this.cachedRange = [
×
84
      aroundTokens.before.range[1],
85
      aroundTokens.after.range[0],
86
    ]);
87
  }
88

89
  public get aroundTokens(): AroundTokens {
90
    if (this.cachedAroundTokens) {
441✔
91
      return this.cachedAroundTokens;
222✔
92
    }
93
    const sourceCode = this.sequence.sourceCode;
219✔
94
    if (this.node) {
219✔
95
      return (this.cachedAroundTokens = {
219✔
96
        before: sourceCode.getTokenBefore(this.node)!,
97
        after: sourceCode.getTokenAfter(this.node)!,
98
      });
99
    }
100
    const before =
101
      this.index > 0
×
102
        ? this.sequence.entries[this.index - 1].aroundTokens.after
103
        : sourceCode.getFirstToken(this.sequence.node);
104
    const after = sourceCode.getTokenAfter(before)!;
×
105
    return (this.cachedAroundTokens = { before, after });
×
106
  }
107

108
  public constructor(
109
    sequence: YAMLSequenceData,
110
    node: YAMLEntry,
111
    index: number,
112
    anchorAlias: {
113
      anchors: Set<string>;
114
      aliases: Set<string>;
115
    },
116
  ) {
117
    this.sequence = sequence;
598✔
118
    this.node = node;
598✔
119
    this.index = index;
598✔
120
    this.anchorAlias = anchorAlias;
598✔
121
  }
122

123
  public get value() {
124
    return (
3,268✔
125
      this.cached ??
3,850✔
126
      (this.cached = {
127
        value: this.node == null ? null : getStaticYAMLValue(this.node),
582✔
128
      })
129
    ).value;
130
  }
131
}
132
class YAMLSequenceData {
133
  public readonly node: AST.YAMLSequence;
134

135
  public readonly sourceCode: SourceCode;
136

137
  private readonly anchorAliasMap: Map<
138
    AST.YAMLContent | AST.YAMLWithMeta | null,
139
    { anchors: Set<string>; aliases: Set<string> }
140
  >;
141

142
  private cachedEntries: YAMLEntryData[] | null = null;
211✔
143

144
  public constructor(
145
    node: AST.YAMLSequence,
146
    sourceCode: SourceCode,
147
    anchorAliasMap: Map<
148
      YAMLEntry,
149
      {
150
        anchors: Set<string>;
151
        aliases: Set<string>;
152
      }
153
    >,
154
  ) {
155
    this.node = node;
211✔
156
    this.sourceCode = sourceCode;
211✔
157
    this.anchorAliasMap = anchorAliasMap;
211✔
158
  }
159

160
  public get entries() {
161
    return (this.cachedEntries ??= this.node.entries.map(
267✔
162
      (e, index) =>
163
        new YAMLEntryData(this, e, index, this.anchorAliasMap.get(e)!),
598✔
164
    ));
165
  }
166
}
167

168
/**
169
 * Build function which check that the given 2 names are in specific order.
170
 */
171
function buildValidatorFromType(
172
  order: OrderTypeOption,
173
  insensitive: boolean,
174
  natural: boolean,
175
): Validator {
176
  type Compare<T> = ([a, b]: T[]) => boolean;
177

178
  // eslint-disable-next-line func-style -- ignore
179
  let compareValue: Compare<
180
    /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore */
181
    any
182
  > = ([a, b]) => a <= b;
577✔
183
  let compareText: Compare<string> = compareValue;
134✔
184

185
  if (natural) {
134✔
186
    compareText = ([a, b]) => naturalCompare(a, b) <= 0;
12✔
187
  }
188
  if (insensitive) {
134✔
189
    const baseCompareText = compareText;
3✔
190
    compareText = ([a, b]: string[]) =>
3✔
191
      baseCompareText([a.toLowerCase(), b.toLowerCase()]);
25✔
192
  }
193
  if (order === "desc") {
134✔
194
    const baseCompareText = compareText;
78✔
195
    compareText = (args: string[]) => baseCompareText(args.reverse());
297✔
196
    const baseCompareValue = compareValue;
78✔
197
    compareValue = (args) => baseCompareValue(args.reverse());
78✔
198
  }
199
  return (a: YAMLEntryData, b: YAMLEntryData) => {
134✔
200
    if (typeof a.value === "string" && typeof b.value === "string") {
703✔
201
      return compareText([a.value, b.value]);
577✔
202
    }
203
    const type = getYAMLPrimitiveType(a.value);
126✔
204
    if (type && type === getYAMLPrimitiveType(b.value)) {
126✔
205
      return compareValue([a.value, b.value]);
12✔
206
    }
207
    // Unknown
208
    return true;
114✔
209
  };
210
}
211

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

228
      return {
134✔
229
        isTargetArray,
230
        ignore: () => false,
598✔
231
        isValidOrder: buildValidatorFromType(type, insensitive, natural),
232
        orderText(data) {
233
          if (typeof data.value === "string") {
106✔
234
            return `${natural ? "natural " : ""}${
102✔
235
              insensitive ? "insensitive " : ""
102✔
236
            }${type}ending`;
237
          }
238
          return `${type}ending`;
4✔
239
        },
240
      };
241
    }
242
    const parsedOrder: {
243
      test: (v: YAMLEntryData) => boolean;
244
      isValidNestOrder: Validator;
245
    }[] = [];
×
246
    for (const o of order) {
×
247
      if (typeof o === "string") {
×
248
        parsedOrder.push({
×
249
          test: (v) => v.value === o,
×
250
          isValidNestOrder: () => true,
×
251
        });
252
      } else {
253
        const valuePattern = o.valuePattern ? new RegExp(o.valuePattern) : null;
×
254
        const nestOrder = o.order ?? {};
×
255
        const type: OrderTypeOption = nestOrder.type ?? "asc";
×
256
        const insensitive = nestOrder.caseSensitive === false;
×
257
        const natural = Boolean(nestOrder.natural);
×
258
        parsedOrder.push({
×
259
          test: (v) =>
260
            valuePattern
×
261
              ? Boolean(getYAMLPrimitiveType(v.value)) &&
×
262
                valuePattern.test(String(v.value))
263
              : true,
264
          isValidNestOrder: buildValidatorFromType(type, insensitive, natural),
265
        });
266
      }
267
    }
268

269
    return {
×
270
      isTargetArray,
271
      ignore: (v) => parsedOrder.every((p) => !p.test(v)),
×
272
      isValidOrder(a, b) {
273
        for (const p of parsedOrder) {
×
274
          const matchA = p.test(a);
×
275
          const matchB = p.test(b);
×
276
          if (!matchA || !matchB) {
×
277
            if (matchA) {
×
278
              return true;
×
279
            }
280
            if (matchB) {
×
281
              return false;
×
282
            }
283
            continue;
×
284
          }
285
          return p.isValidNestOrder(a, b);
×
286
        }
287
        return false;
×
288
      },
289
      orderText: () => "specified",
×
290
    };
291

292
    /**
293
     * Checks whether given node data is verify target
294
     */
295
    function isTargetArray(data: YAMLSequenceData) {
296
      if (data.node.entries.length < minValues) {
211✔
297
        return false;
10✔
298
      }
299

300
      // Check whether the path is match or not.
301
      let path = "";
201✔
302
      let curr: AST.YAMLNode = data.node;
201✔
303
      let p: AST.YAMLNode | null = curr.parent;
201✔
304
      while (p) {
201✔
305
        if (p.type === "YAMLPair") {
642✔
306
          const name = getPropertyName(p);
96✔
307
          if (/^[$a-z_][\w$]*$/iu.test(name)) {
96✔
308
            path = `.${name}${path}`;
60✔
309
          } else {
310
            path = `[${JSON.stringify(name)}]${path}`;
36✔
311
          }
312
        } else if (p.type === "YAMLSequence") {
546✔
313
          const index = p.entries.indexOf(curr as never);
34✔
314
          path = `[${index}]${path}`;
34✔
315
        }
316
        curr = p;
642✔
317
        p = curr.parent;
642✔
318
      }
319
      if (path.startsWith(".")) {
201✔
320
        path = path.slice(1);
56✔
321
      }
322
      return pathPattern.test(path);
201✔
323
    }
324
  });
325

326
  /**
327
   * Gets the property name of the given `YAMLPair` node.
328
   */
329
  function getPropertyName(node: AST.YAMLPair): string {
330
    const prop = node.key;
96✔
331
    if (prop == null) {
96!
332
      return "";
×
333
    }
334
    const target = prop.type === "YAMLWithMeta" ? prop.value : prop;
96✔
335
    if (target == null) {
96!
336
      return "";
×
337
    }
338
    if (target.type === "YAMLScalar" && typeof target.value === "string") {
96✔
339
      return target.value;
70✔
340
    }
341
    return sourceCode.text.slice(...target.range);
26✔
342
  }
343
}
344

345
/**
346
 * Get the type name from given value when value is primitive like value
347
 */
348
function getYAMLPrimitiveType(val: YAMLValue) {
349
  const t = typeof val;
432✔
350
  if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
432✔
351
    return t;
370✔
352
  }
353
  if (val === null) {
62✔
354
    return "null";
2✔
355
  }
356
  if (val === undefined) {
60!
357
    return "undefined";
×
358
  }
359
  if (val instanceof RegExp) {
60!
360
    return "regexp";
×
361
  }
362
  return null;
60✔
363
}
364

365
const ALLOW_ORDER_TYPES: OrderTypeOption[] = ["asc", "desc"];
1✔
366
const ORDER_OBJECT_SCHEMA = {
1✔
367
  type: "object",
368
  properties: {
369
    type: {
370
      enum: ALLOW_ORDER_TYPES,
371
    },
372
    caseSensitive: {
373
      type: "boolean",
374
    },
375
    natural: {
376
      type: "boolean",
377
    },
378
  },
379
  additionalProperties: false,
380
} as const;
381

382
//------------------------------------------------------------------------------
383
// Rule Definition
384
//------------------------------------------------------------------------------
385

386
export default createRule("sort-sequence-values", {
1✔
387
  meta: {
388
    docs: {
389
      description: "require sequence values to be sorted",
390
      categories: null,
391
      extensionRule: false,
392
      layout: false,
393
    },
394
    fixable: "code",
395
    schema: {
396
      type: "array",
397
      items: {
398
        type: "object",
399
        properties: {
400
          pathPattern: { type: "string" },
401
          order: {
402
            oneOf: [
403
              {
404
                type: "array",
405
                items: {
406
                  anyOf: [
407
                    { type: "string" },
408
                    {
409
                      type: "object",
410
                      properties: {
411
                        valuePattern: {
412
                          type: "string",
413
                        },
414
                        order: ORDER_OBJECT_SCHEMA,
415
                      },
416
                      additionalProperties: false,
417
                    },
418
                  ],
419
                },
420
                uniqueItems: true,
421
              },
422
              ORDER_OBJECT_SCHEMA,
423
            ],
424
          },
425
          minValues: {
426
            type: "integer",
427
            minimum: 2,
428
          },
429
        },
430
        required: ["pathPattern", "order"],
431
        additionalProperties: false,
432
      },
433
      minItems: 1,
434
    },
435

436
    messages: {
437
      shouldBeBefore:
438
        "Expected sequence values to be in {{orderText}} order. '{{thisValue}}' should be before '{{targetValue}}'.",
439
      shouldBeAfter:
440
        "Expected sequence values to be in {{orderText}} order. '{{thisValue}}' should be after '{{targetValue}}'.",
441
    },
442
    type: "suggestion",
443
  },
444
  create(context) {
445
    const sourceCode = getSourceCode(context);
134✔
446
    if (!sourceCode.parserServices?.isYAML) {
134!
447
      return {};
×
448
    }
449
    // Parse options.
450
    const parsedOptions = parseOptions(context.options, sourceCode);
134✔
451

452
    /**
453
     * Checks whether the given two entries are in should be kept order.
454
     */
455
    function shouldKeepOrder(prevData: YAMLEntryData, nextData: YAMLEntryData) {
456
      if (
199✔
457
        (prevData.anchorAlias.aliases.size === 0 &&
425✔
458
          prevData.anchorAlias.anchors.size === 0) ||
459
        (nextData.anchorAlias.aliases.size === 0 &&
460
          nextData.anchorAlias.anchors.size === 0)
461
      )
462
        return false;
189✔
463
      for (const aliasName of nextData.anchorAlias.aliases) {
10✔
464
        if (prevData.anchorAlias.anchors.has(aliasName)) {
6✔
465
          // The current order is correct for handling anchors.
466
          return true;
2✔
467
        }
468
      }
469
      for (const anchorName of nextData.anchorAlias.anchors) {
8✔
470
        if (prevData.anchorAlias.aliases.has(anchorName)) {
4✔
471
          // The current order is correct for handling anchors.
472
          return true;
1✔
473
        }
474
      }
475
      return false;
7✔
476
    }
477

478
    /**
479
     * Sort entries by bubble sort.
480
     */
481
    function bubbleSort(entries: YAMLEntryData[], option: ParsedOption) {
482
      const l = entries.length;
201✔
483
      const result = [...entries];
201✔
484
      let swapped: boolean;
485
      do {
201✔
486
        swapped = false;
323✔
487
        for (let nextIndex = 1; nextIndex < l; nextIndex++) {
323✔
488
          const prevIndex = nextIndex - 1;
701✔
489
          if (
701✔
490
            option.isValidOrder(result[prevIndex], result[nextIndex]) ||
851✔
491
            shouldKeepOrder(result[prevIndex], result[nextIndex])
492
          )
493
            continue;
552✔
494
          [result[prevIndex], result[nextIndex]] = [
149✔
495
            result[nextIndex],
496
            result[prevIndex],
497
          ];
498
          swapped = true;
149✔
499
        }
500
      } while (swapped);
501
      return result;
201✔
502
    }
503

504
    /**
505
     * Verify for sequence entries
506
     */
507
    function verifyArrayElements(
508
      entries: YAMLEntryData[],
509
      option: ParsedOption,
510
    ) {
511
      const sorted = bubbleSort(entries, option);
201✔
512
      const editScript = calcShortestEditScript(entries, sorted);
201✔
513
      for (let index = 0; index < editScript.length; index++) {
201✔
514
        const edit = editScript[index];
704✔
515
        if (edit.type !== "delete") continue;
704✔
516
        const insertEditIndex = editScript.findIndex(
106✔
517
          (e) => e.type === "insert" && e.b === edit.a,
379✔
518
        );
519
        if (insertEditIndex === -1) {
106!
520
          // should not happen
NEW
521
          continue;
×
522
        }
523
        if (index < insertEditIndex) {
106✔
524
          const target = findInsertAfterTarget(edit.a, insertEditIndex);
93✔
525
          if (!target) {
93!
526
            // should not happen
NEW
527
            continue;
×
528
          }
529
          context.report({
93✔
530
            loc: edit.a.reportLoc,
531
            messageId: "shouldBeAfter",
532
            data: {
533
              thisValue: toText(edit.a),
534
              targetValue: toText(target),
535
              orderText: option.orderText(edit.a),
536
            },
537
            *fix(fixer) {
538
              if (edit.a.sequence.node.style === "flow") {
93✔
539
                yield* fixToMoveDownForFlow(fixer, edit.a, target);
36✔
540
              } else {
541
                yield* fixToMoveDownForBlock(fixer, edit.a, target);
57✔
542
              }
543
            },
544
          });
545
        } else {
546
          const target = findInsertBeforeTarget(edit.a, insertEditIndex);
13✔
547
          if (!target) {
13!
548
            // should not happen
NEW
549
            continue;
×
550
          }
551
          context.report({
13✔
552
            loc: edit.a.reportLoc,
553
            messageId: "shouldBeBefore",
554
            data: {
555
              thisValue: toText(edit.a),
556
              targetValue: toText(target),
557
              orderText: option.orderText(edit.a),
558
            },
559
            *fix(fixer) {
560
              if (edit.a.sequence.node.style === "flow") {
13✔
561
                yield* fixToMoveUpForFlow(fixer, edit.a, target);
4✔
562
              } else {
563
                yield* fixToMoveUpForBlock(fixer, edit.a, target);
9✔
564
              }
565
            },
566
          });
567
        }
568
      }
569

570
      /**
571
       * Find insert after target
572
       */
573
      function findInsertAfterTarget(
574
        entry: YAMLEntryData,
575
        insertEditIndex: number,
576
      ) {
577
        let candidate: YAMLEntryData | null = null;
93✔
578
        for (let index = insertEditIndex - 1; index >= 0; index--) {
93✔
579
          const edit = editScript[index];
106✔
580
          if (edit.type === "delete" && edit.a === entry) break;
106!
581
          if (edit.type !== "common") continue;
106✔
582
          candidate = edit.a;
93✔
583
          break;
93✔
584
        }
585
        const entryIndex = entries.indexOf(entry);
93✔
586
        if (candidate) {
93✔
587
          for (let index = entryIndex + 1; index < entries.length; index++) {
93✔
588
            const element = entries[index];
120✔
589
            if (element === candidate) return candidate;
120✔
590
            if (shouldKeepOrder(entry, element)) {
28✔
591
              break;
1✔
592
            }
593
          }
594
        }
595

596
        let lastTarget: YAMLEntryData | null = null;
1✔
597
        for (let index = entryIndex + 1; index < entries.length; index++) {
1✔
598
          const element = entries[index];
2✔
599
          if (
2✔
600
            option.isValidOrder(element, entry) &&
4✔
601
            !shouldKeepOrder(entry, element)
602
          ) {
603
            lastTarget = element;
1✔
604
            continue;
1✔
605
          }
606
          return lastTarget;
1✔
607
        }
NEW
608
        return lastTarget;
×
609
      }
610

611
      /**
612
       * Find insert before target
613
       */
614
      function findInsertBeforeTarget(
615
        entry: YAMLEntryData,
616
        insertEditIndex: number,
617
      ) {
618
        let candidate: YAMLEntryData | null = null;
13✔
619
        for (
13✔
620
          let index = insertEditIndex + 1;
13✔
621
          index < editScript.length;
622
          index++
623
        ) {
624
          const edit = editScript[index];
14✔
625
          if (edit.type === "delete" && edit.a === entry) break;
14!
626
          if (edit.type !== "common") continue;
14✔
627
          candidate = edit.a;
13✔
628
          break;
13✔
629
        }
630
        const entryIndex = entries.indexOf(entry);
13✔
631
        if (candidate) {
13✔
632
          for (let index = entryIndex - 1; index >= 0; index--) {
13✔
633
            const element = entries[index];
32✔
634
            if (element === candidate) return candidate;
32✔
635
            if (shouldKeepOrder(element, entry)) {
19!
NEW
636
              break;
×
637
            }
638
          }
639
        }
640

NEW
641
        let lastTarget: YAMLEntryData | null = null;
×
NEW
642
        for (let index = entryIndex - 1; index >= 0; index--) {
×
NEW
643
          const element = entries[index];
×
NEW
644
          if (
×
645
            option.isValidOrder(entry, element) &&
×
646
            !shouldKeepOrder(element, entry)
647
          ) {
NEW
648
            lastTarget = element;
×
NEW
649
            continue;
×
650
          }
NEW
651
          return lastTarget;
×
652
        }
NEW
653
        return lastTarget;
×
654
      }
655
    }
656

657
    /**
658
     * Convert to display text.
659
     */
660
    function toText(data: YAMLEntryData) {
661
      if (getYAMLPrimitiveType(data.value)) {
212✔
662
        return String(data.value);
212✔
663
      }
664
      return sourceCode.getText(data.node!);
×
665
    }
666

667
    type EntryStack = {
668
      upper: EntryStack | null;
669
      anchors: Set<string>;
670
      aliases: Set<string>;
671
    };
672
    let entryStack: EntryStack = {
134✔
673
      upper: null,
674
      anchors: new Set<string>(),
675
      aliases: new Set<string>(),
676
    };
677
    const anchorAliasMap = new Map<
134✔
678
      YAMLEntry,
679
      {
680
        anchors: Set<string>;
681
        aliases: Set<string>;
682
      }
683
    >();
684

685
    return {
134✔
686
      "YAMLSequence > *"(node: YAMLEntry & { parent: AST.YAMLSequence }) {
687
        if (!node.parent.entries.includes(node)) {
604!
688
          return;
×
689
        }
690
        entryStack = {
604✔
691
          upper: entryStack,
692
          anchors: new Set<string>(),
693
          aliases: new Set<string>(),
694
        };
695
        if (node.type === "YAMLAlias") {
604✔
696
          entryStack.aliases.add(node.name);
22✔
697
        }
698
      },
699
      YAMLAnchor(node: AST.YAMLAnchor) {
700
        if (entryStack) {
40✔
701
          entryStack.anchors.add(node.name);
40✔
702
        }
703
      },
704
      YAMLAlias(node: AST.YAMLAlias) {
705
        if (entryStack) {
30✔
706
          entryStack.aliases.add(node.name);
30✔
707
        }
708
      },
709
      "YAMLSequence > *:exit"(node: YAMLEntry & { parent: AST.YAMLSequence }) {
710
        if (!node.parent.entries.includes(node)) {
604!
711
          return;
×
712
        }
713
        anchorAliasMap.set(node, entryStack);
604✔
714
        const { anchors, aliases } = entryStack;
604✔
715
        entryStack = entryStack.upper!;
604✔
716
        entryStack.anchors = new Set([...entryStack.anchors, ...anchors]);
604✔
717
        entryStack.aliases = new Set([...entryStack.aliases, ...aliases]);
604✔
718
      },
719
      "YAMLSequence:exit"(node: AST.YAMLSequence) {
720
        const data = new YAMLSequenceData(node, sourceCode, anchorAliasMap);
211✔
721
        const option = parsedOptions.find((o) => o.isTargetArray(data));
211✔
722
        if (!option) {
211✔
723
          return;
10✔
724
        }
725
        verifyArrayElements(
201✔
726
          data.entries.filter((d) => !option.ignore(d)),
598✔
727
          option,
728
        );
729
      },
730
    };
731

732
    /**
733
     * Fix by moving the node after the target node for flow.
734
     */
735
    function* fixToMoveDownForFlow(
736
      fixer: RuleFixer,
737
      data: YAMLEntryData,
738
      moveTarget: YAMLEntryData,
739
    ) {
740
      const beforeToken = data.aroundTokens.before;
36✔
741
      const afterToken = data.aroundTokens.after;
36✔
742
      let insertCode: string,
743
        removeRange: AST.Range,
744
        insertTargetToken: YAMLToken;
745
      if (isComma(afterToken)) {
36!
746
        // e.g. |# comment\n value,|
747
        removeRange = [beforeToken.range[1], afterToken.range[1]];
36✔
748
        const moveTargetAfterToken = moveTarget.aroundTokens.after;
36✔
749
        if (isComma(moveTargetAfterToken)) {
36✔
750
          // e.g. value,
751
          insertTargetToken = moveTargetAfterToken;
14✔
752
          insertCode = sourceCode.text.slice(...removeRange);
14✔
753
        } else {
754
          // e.g. value]
755
          insertTargetToken = moveTarget.node
22!
756
            ? sourceCode.getLastToken(moveTarget.node)
757
            : moveTarget.aroundTokens.before;
758
          insertCode = sourceCode.text.slice(
22✔
759
            beforeToken.range[1],
760
            afterToken.range[0],
761
          );
762
          insertCode = `,${insertCode}`;
22✔
763
        }
764
      } else {
NEW
765
        if (isComma(beforeToken)) {
×
766
          // e.g. |, # comment\n value|
NEW
767
          removeRange = [beforeToken.range[0], data.range[1]];
×
NEW
768
          insertCode = sourceCode.text.slice(...removeRange);
×
NEW
769
          insertTargetToken = moveTarget.node
×
770
            ? sourceCode.getLastToken(moveTarget.node)
771
            : moveTarget.aroundTokens.before;
772
        } else {
773
          // e.g. |[# comment\n value|
NEW
774
          removeRange = [beforeToken.range[1], data.range[1]];
×
NEW
775
          insertCode = `,${sourceCode.text.slice(...removeRange)}`;
×
NEW
776
          insertTargetToken = moveTarget.node
×
777
            ? sourceCode.getLastToken(moveTarget.node)
778
            : moveTarget.aroundTokens.before;
779
        }
780
      }
781
      yield fixer.removeRange(removeRange);
36✔
782
      yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
36✔
783
    }
784

785
    /**
786
     * Fix by moving the node before the target node for flow.
787
     */
788
    function* fixToMoveUpForFlow(
789
      fixer: RuleFixer,
790
      data: YAMLEntryData,
791
      moveTarget: YAMLEntryData,
792
    ) {
793
      const beforeToken = data.aroundTokens.before;
4✔
794
      const afterToken = data.aroundTokens.after;
4✔
795
      let insertCode: string,
796
        removeRange: AST.Range,
797
        insertTargetToken: YAMLToken;
798
      if (isComma(afterToken)) {
4✔
799
        // e.g. |# comment\n value,|
800
        removeRange = [beforeToken.range[1], afterToken.range[1]];
2✔
801
        insertCode = sourceCode.text.slice(...removeRange);
2✔
802
        insertTargetToken = moveTarget.aroundTokens.before;
2✔
803
      } else {
804
        // e.g. |, # comment\n value|
805
        removeRange = [beforeToken.range[0], data.range[1]];
2✔
806
        if (isComma(moveTarget.aroundTokens.before)) {
2!
807
          // [ a , target ]
808
          //    ^ insert
UNCOV
809
          insertCode = sourceCode.text.slice(...removeRange);
×
UNCOV
810
          insertTargetToken = sourceCode.getTokenBefore(
×
811
            moveTarget.aroundTokens.before,
812
          )!;
813
        } else {
814
          // [ target ]
815
          //  ^ insert
816
          insertCode = `${sourceCode.text.slice(
2✔
817
            beforeToken.range[1],
818
            data.range[1],
819
          )},`;
820
          insertTargetToken = moveTarget.aroundTokens.before;
2✔
821
        }
822
      }
823
      yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
4✔
824

825
      yield fixer.removeRange(removeRange);
4✔
826
    }
827

828
    /**
829
     * Fix by moving the node after the target node for block.
830
     */
831
    function* fixToMoveDownForBlock(
832
      fixer: RuleFixer,
833
      data: YAMLEntryData,
834
      moveTarget: YAMLEntryData,
835
    ) {
836
      const moveDataList = data.sequence.entries.slice(
57✔
837
        data.index,
838
        moveTarget.index + 1,
839
      );
840

841
      let replacementCodeRange = getBlockEntryRange(data);
57✔
842
      for (const target of moveDataList.reverse()) {
57✔
843
        const range = getBlockEntryRange(target);
135✔
844
        yield fixer.replaceTextRange(
135✔
845
          range,
846
          sourceCode.text.slice(...replacementCodeRange),
847
        );
848
        replacementCodeRange = range;
135✔
849
      }
850
    }
851

852
    /**
853
     * Fix by moving the node before the target node for block.
854
     */
855
    function* fixToMoveUpForBlock(
856
      fixer: RuleFixer,
857
      data: YAMLEntryData,
858
      moveTarget: YAMLEntryData,
859
    ) {
860
      const moveDataList = data.sequence.entries.slice(
9✔
861
        moveTarget.index,
862
        data.index + 1,
863
      );
864

865
      let replacementCodeRange = getBlockEntryRange(data);
9✔
866
      for (const target of moveDataList) {
9✔
867
        const range = getBlockEntryRange(target);
33✔
868
        yield fixer.replaceTextRange(
33✔
869
          range,
870
          sourceCode.text.slice(...replacementCodeRange),
871
        );
872
        replacementCodeRange = range;
33✔
873
      }
874
    }
875

876
    /**
877
     * Get range of entry
878
     */
879
    function getBlockEntryRange(data: YAMLEntryData): AST.Range {
880
      return [getBlockEntryStartOffset(data), getBlockEntryEndOffset(data)];
234✔
881
    }
882

883
    /**
884
     * Get start offset of entry
885
     */
886
    function getBlockEntryStartOffset(data: YAMLEntryData) {
887
      const beforeHyphenToken = sourceCode.getTokenBefore(
234✔
888
        data.aroundTokens.before,
889
      );
890
      if (!beforeHyphenToken) {
234✔
891
        const comment = sourceCode.getTokenBefore(data.aroundTokens.before, {
34✔
892
          includeComments: true,
893
        });
894
        if (
34✔
895
          comment &&
63✔
896
          data.aroundTokens.before.loc.start.column <= comment.loc.start.column
897
        ) {
898
          return comment.range[0];
27✔
899
        }
900

901
        return data.aroundTokens.before.range[0];
7✔
902
      }
903
      let next = sourceCode.getTokenAfter(beforeHyphenToken, {
200✔
904
        includeComments: true,
905
      })!;
906
      while (
200✔
907
        beforeHyphenToken.loc.end.line === next.loc.start.line &&
220✔
908
        next.range[1] < data.aroundTokens.before.range[0]
909
      ) {
910
        next = sourceCode.getTokenAfter(next, {
5✔
911
          includeComments: true,
912
        })!;
913
      }
914
      return next.range[0];
200✔
915
    }
916

917
    /**
918
     * Get start offset of entry
919
     */
920
    function getBlockEntryEndOffset(data: YAMLEntryData) {
921
      const valueEndToken = data.node ?? data.aroundTokens.before;
234!
922
      let last = valueEndToken;
234✔
923
      let afterToken = sourceCode.getTokenAfter(last, {
234✔
924
        includeComments: true,
925
      });
926
      while (
234✔
927
        afterToken &&
449✔
928
        valueEndToken.loc.end.line === afterToken.loc.start.line
929
      ) {
930
        last = afterToken;
8✔
931
        afterToken = sourceCode.getTokenAfter(last, {
8✔
932
          includeComments: true,
933
        });
934
      }
935
      return last.range[1];
234✔
936
    }
937
  },
938
});
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