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

ota-meshi / eslint-plugin-yml / 7834400087

08 Feb 2024 06:43PM CUT coverage: 90.377%. Remained the same
7834400087

push

github

renovate[bot]
chore(deps): update dependency esbuild to ^0.20.0

1604 of 1853 branches covered (0.0%)

Branch coverage included in aggregate %.

2284 of 2449 relevant lines covered (93.26%)

623.05 hits per line

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

76.72
/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
import { getSourceCode } from "../utils/compat";
1✔
8

9
type YAMLValue = ReturnType<typeof getStaticYAMLValue>;
10

11
//------------------------------------------------------------------------------
12
// Helpers
13
//------------------------------------------------------------------------------
14

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

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

48
  public readonly node: YAMLEntry;
49

50
  public readonly index: number;
51

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

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

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

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

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

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

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

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

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

134
  public readonly sourceCode: SourceCode;
135

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

381
//------------------------------------------------------------------------------
382
// Rule Definition
383
//------------------------------------------------------------------------------
384

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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