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

ota-meshi / eslint-plugin-yml / 3637341569

pending completion
3637341569

push

github

Yosuke Ota
refactor

1462 of 1709 branches covered (85.55%)

Branch coverage included in aggregate %.

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

2104 of 2271 relevant lines covered (92.65%)

543.49 hits per line

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

76.66
/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";
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

8
type YAMLValue = ReturnType<typeof getStaticYAMLValue>;
9

10
//------------------------------------------------------------------------------
11
// Helpers
12
//------------------------------------------------------------------------------
13

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

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

47
  public readonly node: YAMLEntry;
48

49
  public readonly index: number;
50

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

56
  private cached: { value: YAMLValue } | null = null;
598✔
57

58
  private cachedRange: [number, number] | null = null;
598✔
59

60
  private cachedAroundTokens: AroundTokens | null = null;
598✔
61

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

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

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

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

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

133
  public readonly sourceCode: SourceCode;
134

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

140
  private cachedEntries: YAMLEntryData[] | null = null;
211✔
141

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

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

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

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

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

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

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

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

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

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

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

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

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

380
//------------------------------------------------------------------------------
381
// Rule Definition
382
//------------------------------------------------------------------------------
383

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

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

448
    /**
449
     * Check order
450
     */
451
    function isValidOrder(
452
      prevData: YAMLEntryData,
453
      thisData: YAMLEntryData,
454
      option: ParsedOption
455
    ) {
456
      if (option.isValidOrder(prevData, thisData)) {
572✔
457
        return true;
315✔
458
      }
459

460
      for (const aliasName of thisData.anchorAlias.aliases) {
257✔
461
        if (prevData.anchorAlias.anchors.has(aliasName)) {
8!
462
          // The current order is correct for handling anchors.
463
          return true;
×
464
        }
465
      }
466
      for (const anchorName of thisData.anchorAlias.anchors) {
257✔
467
        if (prevData.anchorAlias.aliases.has(anchorName)) {
15✔
468
          // The current order is correct for handling anchors.
469
          return true;
1✔
470
        }
471
      }
472
      return false;
256✔
473
    }
474

475
    /**
476
     * Verify for sequence entries
477
     */
478
    function verifyArrayElement(data: YAMLEntryData, option: ParsedOption) {
479
      if (option.ignore(data)) {
598!
480
        return;
×
481
      }
482
      const prevList = data.sequence.entries
598✔
483
        .slice(0, data.index)
484
        .reverse()
485
        .filter((d) => !option.ignore(d));
715✔
486

487
      if (prevList.length === 0) {
598✔
488
        return;
201✔
489
      }
490
      const prev = prevList[0];
397✔
491
      if (!isValidOrder(prev, data, option)) {
397✔
492
        const reportLoc = data.reportLoc;
109✔
493
        context.report({
109✔
494
          loc: reportLoc,
495
          messageId: "sortValues",
496
          data: {
497
            thisValue: toText(data),
498
            prevValue: toText(prev),
499
            orderText: option.orderText(data),
500
          },
501
          *fix(fixer) {
502
            let moveTarget = prevList[0];
109✔
503
            for (const prev of prevList) {
109✔
504
              if (isValidOrder(prev, data, option)) {
175✔
505
                break;
28✔
506
              } else {
507
                moveTarget = prev;
147✔
508
              }
509
            }
510
            if (data.sequence.node.style === "flow") {
109✔
511
              yield* fixForFlow(fixer, data, moveTarget);
41✔
512
            } else {
513
              yield* fixForBlock(fixer, data, moveTarget);
68✔
514
            }
515
          },
516
        });
517
      }
518
    }
519

520
    /**
521
     * Convert to display text.
522
     */
523
    function toText(data: YAMLEntryData) {
524
      if (getYAMLPrimitiveType(data.value)) {
218!
525
        return String(data.value);
218✔
526
      }
527
      return sourceCode.getText(data.node!);
×
528
    }
529

530
    type EntryStack = {
531
      upper: EntryStack | null;
532
      anchors: Set<string>;
533
      aliases: Set<string>;
534
    };
535
    let entryStack: EntryStack = {
134✔
536
      upper: null,
537
      anchors: new Set<string>(),
538
      aliases: new Set<string>(),
539
    };
540
    const anchorAliasMap = new Map<
134✔
541
      YAMLEntry,
542
      {
543
        anchors: Set<string>;
544
        aliases: Set<string>;
545
      }
546
    >();
547

548
    return {
134✔
549
      "YAMLSequence > *"(node: YAMLEntry & { parent: AST.YAMLSequence }) {
550
        if (!node.parent.entries.includes(node)) {
604!
551
          return;
×
552
        }
553
        entryStack = {
604✔
554
          upper: entryStack,
555
          anchors: new Set<string>(),
556
          aliases: new Set<string>(),
557
        };
558
        if (node.type === "YAMLAlias") {
604✔
559
          entryStack.aliases.add(node.name);
22✔
560
        }
561
      },
562
      YAMLAnchor(node: AST.YAMLAnchor) {
563
        if (entryStack) {
40!
564
          entryStack.anchors.add(node.name);
40✔
565
        }
566
      },
567
      YAMLAlias(node: AST.YAMLAlias) {
568
        if (entryStack) {
30!
569
          entryStack.aliases.add(node.name);
30✔
570
        }
571
      },
572
      "YAMLSequence > *:exit"(node: YAMLEntry & { parent: AST.YAMLSequence }) {
573
        if (!node.parent.entries.includes(node)) {
604!
574
          return;
×
575
        }
576
        anchorAliasMap.set(node, entryStack);
604✔
577
        const { anchors, aliases } = entryStack;
604✔
578
        entryStack = entryStack.upper!;
604✔
579
        entryStack.anchors = new Set([...entryStack.anchors, ...anchors]);
604✔
580
        entryStack.aliases = new Set([...entryStack.aliases, ...aliases]);
604✔
581
      },
582
      "YAMLSequence:exit"(node: AST.YAMLSequence) {
583
        const data = new YAMLSequenceData(node, sourceCode, anchorAliasMap);
211✔
584
        const option = parsedOptions.find((o) => o.isTargetArray(data));
211✔
585
        if (!option) {
211✔
586
          return;
10✔
587
        }
588
        for (const element of data.entries) {
201✔
589
          verifyArrayElement(element, option);
598✔
590
        }
591
      },
592
    };
593

594
    /**
595
     * Fix for flow
596
     */
597
    function* fixForFlow(
598
      fixer: RuleFixer,
599
      data: YAMLEntryData,
600
      moveTarget: YAMLEntryData
601
    ) {
602
      const beforeToken = data.aroundTokens.before;
41✔
603
      const afterToken = data.aroundTokens.after;
41✔
604
      let insertCode: string,
605
        removeRange: AST.Range,
606
        insertTargetToken: YAMLToken;
607
      if (isComma(afterToken)) {
41✔
608
        // e.g. |# comment\n value,|
609
        removeRange = [beforeToken.range[1], afterToken.range[1]];
17✔
610
        insertCode = sourceCode.text.slice(...removeRange);
17✔
611
        insertTargetToken = moveTarget.aroundTokens.before;
17✔
612
      } else {
613
        // e.g. |, # comment\n value|
614
        removeRange = [beforeToken.range[0], data.range[1]];
24✔
615
        if (isComma(moveTarget.aroundTokens.before)) {
24✔
616
          // [ a , target ]
617
          //    ^ insert
618
          insertCode = sourceCode.text.slice(...removeRange);
2✔
619
          insertTargetToken = sourceCode.getTokenBefore(
2✔
620
            moveTarget.aroundTokens.before
621
          )!;
622
        } else {
623
          // [ target ]
624
          //  ^ insert
625
          insertCode = `${sourceCode.text.slice(
22✔
626
            beforeToken.range[1],
627
            data.range[1]
628
          )},`;
629
          insertTargetToken = moveTarget.aroundTokens.before;
22✔
630
        }
631
      }
632
      yield fixer.insertTextAfterRange(insertTargetToken.range, insertCode);
41✔
633

634
      yield fixer.removeRange(removeRange);
41✔
635
    }
636

637
    /**
638
     * Fix for block
639
     */
640
    function* fixForBlock(
641
      fixer: RuleFixer,
642
      data: YAMLEntryData,
643
      moveTarget: YAMLEntryData
644
    ) {
645
      const moveDataList = data.sequence.entries.slice(
68✔
646
        moveTarget.index,
647
        data.index + 1
648
      );
649

650
      let replacementCodeRange = getBlockEntryRange(data);
68✔
651
      for (const target of moveDataList) {
68✔
652
        const range = getBlockEntryRange(target);
162✔
653
        yield fixer.replaceTextRange(
162✔
654
          range,
655
          sourceCode.text.slice(...replacementCodeRange)
656
        );
657
        replacementCodeRange = range;
162✔
658
      }
659
    }
660

661
    /**
662
     * Get range of entry
663
     */
664
    function getBlockEntryRange(data: YAMLEntryData): AST.Range {
665
      return [getBlockEntryStartOffset(data), getBlockEntryEndOffset(data)];
230✔
666
    }
667

668
    /**
669
     * Get start offset of entry
670
     */
671
    function getBlockEntryStartOffset(data: YAMLEntryData) {
672
      const beforeHyphenToken = sourceCode.getTokenBefore(
230✔
673
        data.aroundTokens.before
674
      );
675
      if (!beforeHyphenToken) {
230✔
676
        const comment = sourceCode.getTokenBefore(data.aroundTokens.before, {
22✔
677
          includeComments: true,
678
        });
679
        if (
22✔
680
          comment &&
41✔
681
          data.aroundTokens.before.loc.start.column <= comment.loc.start.column
682
        ) {
683
          return comment.range[0];
18✔
684
        }
685

686
        return data.aroundTokens.before.range[0];
4✔
687
      }
688
      let next = sourceCode.getTokenAfter(beforeHyphenToken, {
208✔
689
        includeComments: true,
690
      })!;
691
      while (
208✔
692
        beforeHyphenToken.loc.end.line === next.loc.start.line &&
227✔
693
        next.range[1] < data.aroundTokens.before.range[0]
694
      ) {
695
        next = sourceCode.getTokenAfter(next, {
7✔
696
          includeComments: true,
697
        })!;
698
      }
699
      return next.range[0];
208✔
700
    }
701

702
    /**
703
     * Get start offset of entry
704
     */
705
    function getBlockEntryEndOffset(data: YAMLEntryData) {
706
      const valueEndToken = data.node ?? data.aroundTokens.before;
230!
707
      let last = valueEndToken;
230✔
708
      let afterToken = sourceCode.getTokenAfter(last, {
230✔
709
        includeComments: true,
710
      });
711
      while (
230✔
712
        afterToken &&
424✔
713
        valueEndToken.loc.end.line === afterToken.loc.start.line
714
      ) {
715
        last = afterToken;
7✔
716
        afterToken = sourceCode.getTokenAfter(last, {
7✔
717
          includeComments: true,
718
        });
719
      }
720
      return last.range[1];
230✔
721
    }
722
  },
723
});
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

© 2025 Coveralls, Inc