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

ota-meshi / eslint-plugin-yml / 21777592258

07 Feb 2026 09:01AM UTC coverage: 89.91% (-0.02%) from 89.929%
21777592258

push

github

web-flow
fix: migrate deprecated apis to support ESLint v10 (#564)

* fix: migrate deprecated apis to support ESLint v10

* docs: changeset

1622 of 1888 branches covered (85.91%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

2949 of 3196 relevant lines covered (92.27%)

1934.1 hits per line

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

83.62
/src/language/yaml-source-code.ts
1
/**
10,759!
2
 * @fileoverview The YAMLSourceCode class.
3
 */
4

5
import { traverseNodes, type AST } from "yaml-eslint-parser";
1✔
6
import type {
7
  TraversalStep,
8
  IDirective as Directive,
9
} from "@eslint/plugin-kit";
10
import {
11
  TextSourceCodeBase,
12
  CallMethodStep,
13
  VisitNodeStep,
14
  ConfigCommentParser,
15
  Directive as DirectiveImpl,
16
} from "@eslint/plugin-kit";
1✔
17
import type { DirectiveType, FileProblem, RulesConfig } from "@eslint/core";
18
import type {
19
  CursorWithCountOptions,
20
  CursorWithSkipOptions,
21
  FilterPredicate,
22
} from "./token-store.js";
23
import { TokenStore } from "./token-store.js";
1✔
24
import type { Scope } from "eslint";
25

26
//-----------------------------------------------------------------------------
27
// Helpers
28
//-----------------------------------------------------------------------------
29

30
const commentParser = new ConfigCommentParser();
1✔
31

32
/**
33
 * Pattern to match ESLint inline configuration comments in YAML.
34
 * Matches: eslint, eslint-disable, eslint-enable, eslint-disable-line, eslint-disable-next-line
35
 */
36
const INLINE_CONFIG =
37
  /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
1✔
38

39
//-----------------------------------------------------------------------------
40
// Types
41
//-----------------------------------------------------------------------------
42
/**
43
 * YAML-specific syntax element type
44
 */
45
export type YAMLSyntaxElement = AST.YAMLNode | AST.Token | AST.Comment;
46
export type YAMLToken = AST.Token | AST.Comment;
47

48
/**
49
 * YAML Source Code Object
50
 */
51
export class YAMLSourceCode extends TextSourceCodeBase<{
52
  LangOptions: Record<never, never>;
53
  RootNode: AST.YAMLProgram;
54
  SyntaxElementWithLoc: YAMLSyntaxElement;
55
  ConfigNode: AST.Comment;
56
}> {
1✔
57
  public readonly hasBOM: boolean;
58

59
  public readonly parserServices: { isYAML?: boolean; parseError?: unknown };
60

61
  public readonly visitorKeys: Record<string, string[]>;
62

63
  private readonly tokenStore: TokenStore;
64

65
  #steps: TraversalStep[] | null = null;
5,368✔
66

67
  #cacheTokensAndComments: (AST.Token | AST.Comment)[] | null = null;
5,368✔
68

69
  #inlineConfigComments: AST.Comment[] | null = null;
5,368✔
70

71
  /**
72
   * Creates a new instance.
73
   */
74
  public constructor(config: {
75
    text: string;
76
    ast: AST.YAMLProgram;
77
    hasBOM: boolean;
78
    parserServices: { isYAML: boolean; parseError?: unknown };
79
    visitorKeys?: Record<string, string[]> | null | undefined;
80
  }) {
81
    super({
5,368✔
82
      ast: config.ast,
83
      text: config.text,
84
    });
85
    this.hasBOM = Boolean(config.hasBOM);
5,368✔
86
    this.parserServices = config.parserServices;
5,368✔
87
    this.visitorKeys = config.visitorKeys || {};
5,368!
88
    this.tokenStore = new TokenStore({ ast: this.ast });
5,368✔
89
  }
90

91
  public traverse(): Iterable<TraversalStep> {
92
    if (this.#steps != null) {
5,343!
93
      return this.#steps;
×
94
    }
95

96
    const steps: (VisitNodeStep | CallMethodStep)[] = [];
5,343✔
97
    this.#steps = steps;
5,343✔
98

99
    const root = this.ast;
5,343✔
100
    steps.push(
5,343✔
101
      // ESLint core rule compatibility: onCodePathStart is called with two arguments.
102
      new CallMethodStep({
103
        target: "onCodePathStart",
104
        args: [{}, root],
105
      }),
106
    );
5,343✔
107

108
    traverseNodes(root, {
109
      enterNode(n) {
110
        steps.push(
94,817✔
111
          new VisitNodeStep({
112
            target: n,
113
            phase: 1,
114
            args: [n],
115
          }),
116
        );
117
      },
118
      leaveNode(n) {
119
        steps.push(
94,817✔
120
          new VisitNodeStep({
121
            target: n,
122
            phase: 2,
123
            args: [n],
124
          }),
125
        );
126
      },
127
    });
128

129
    steps.push(
5,343✔
130
      // ESLint core rule compatibility: onCodePathEnd is called with two arguments.
131
      new CallMethodStep({
132
        target: "onCodePathEnd",
133
        args: [{}, root],
134
      }),
135
    );
136
    return steps;
5,343✔
137
  }
138

139
  /**
140
   * Gets all tokens and comments.
141
   */
142
  public get tokensAndComments(): YAMLToken[] {
143
    return (this.#cacheTokensAndComments ??= [
11✔
144
      ...this.ast.tokens,
145
      ...this.ast.comments,
146
    ].sort((a, b) => a.range[0] - b.range[0]));
447✔
147
  }
148

149
  public getLines(): string[] {
150
    return this.lines;
2,160✔
151
  }
152

153
  public getAllComments(): AST.Comment[] {
154
    return this.ast.comments;
38✔
155
  }
156

157
  /**
158
   * Returns an array of all inline configuration nodes found in the source code.
159
   * This includes eslint-disable, eslint-enable, eslint-disable-line,
160
   * eslint-disable-next-line, and eslint (for inline config) comments.
161
   */
162
  public getInlineConfigNodes(): AST.Comment[] {
163
    if (!this.#inlineConfigComments) {
10,689✔
164
      this.#inlineConfigComments = this.ast.comments.filter((comment) =>
5,346✔
165
        INLINE_CONFIG.test(comment.value),
8,407✔
166
      );
167
    }
168

169
    return this.#inlineConfigComments;
10,689✔
170
  }
171

172
  /**
173
   * Returns directives that enable or disable rules along with any problems
174
   * encountered while parsing the directives.
175
   */
176
  public getDisableDirectives(): {
177
    directives: Directive[];
178
    problems: FileProblem[];
179
  } {
180
    const problems: FileProblem[] = [];
5,343✔
181
    const directives: Directive[] = [];
5,343✔
182

183
    this.getInlineConfigNodes().forEach((comment) => {
5,343✔
184
      const directive = commentParser.parseDirective(comment.value);
22✔
185

186
      if (!directive) {
22!
187
        return;
×
188
      }
189

190
      const { label, value, justification } = directive;
22✔
191

192
      // `eslint-disable-line` directives are not allowed to span multiple lines
193
      // as it would be confusing to which lines they apply
194
      if (
22!
195
        label === "eslint-disable-line" &&
25✔
196
        comment.loc.start.line !== comment.loc.end.line
197
      ) {
198
        const message = `${label} comment should not span multiple lines.`;
×
199

200
        problems.push({
×
201
          ruleId: null,
202
          message,
203
          loc: comment.loc,
204
        });
205
        return;
×
206
      }
207

208
      switch (label) {
22✔
209
        case "eslint-disable":
210
        case "eslint-enable":
211
        case "eslint-disable-next-line":
212
        case "eslint-disable-line": {
213
          const directiveType = label.slice("eslint-".length);
16✔
214

215
          directives.push(
16✔
216
            new DirectiveImpl({
217
              type: directiveType as DirectiveType,
218
              node: comment,
219
              value,
220
              justification,
221
            }),
222
          );
223
          break;
16✔
224
        }
225
        // no default
226
      }
227
    });
228

229
    return { problems, directives };
5,343✔
230
  }
231

232
  /**
233
   * Returns inline rule configurations along with any problems
234
   * encountered while parsing the configurations.
235
   */
236
  public applyInlineConfig(): {
237
    configs: { config: { rules: RulesConfig }; loc: AST.SourceLocation }[];
238
    problems: FileProblem[];
239
  } {
240
    const problems: FileProblem[] = [];
5,343✔
241
    const configs: {
242
      config: { rules: RulesConfig };
243
      loc: AST.SourceLocation;
244
    }[] = [];
5,343✔
245

246
    this.getInlineConfigNodes().forEach((comment) => {
5,343✔
247
      const directive = commentParser.parseDirective(comment.value);
22✔
248

249
      if (!directive) {
22!
250
        return;
×
251
      }
252

253
      const { label, value } = directive;
22✔
254

255
      if (label === "eslint") {
22✔
256
        const parseResult = commentParser.parseJSONLikeConfig(value);
6✔
257

258
        if (parseResult.ok) {
6✔
259
          configs.push({
5✔
260
            config: {
261
              rules: parseResult.config,
262
            },
263
            loc: comment.loc,
264
          });
265
        } else {
266
          problems.push({
1✔
267
            ruleId: null,
268
            message: parseResult.error.message,
269
            loc: comment.loc,
270
          });
271
        }
272
      }
273
    });
274

275
    return { configs, problems };
5,343✔
276
  }
277

278
  public getNodeByRangeIndex(index: number): AST.YAMLNode | null {
279
    let node = find([this.ast]);
15✔
280
    if (!node) return null;
15✔
281
    while (true) {
14✔
282
      const child = find(this._getChildren(node));
79✔
283
      if (!child) return node;
79✔
284
      node = child;
65✔
285
    }
286

287
    /**
288
     * Finds a node that contains the given index.
289
     */
290
    function find(nodes: AST.YAMLNode[]) {
×
291
      for (const node of nodes) {
94✔
292
        if (node.range[0] <= index && index < node.range[1]) {
118✔
293
          return node;
79✔
294
        }
295
      }
296
      return null;
15✔
297
    }
298
  }
299

300
  public getFirstToken(node: YAMLSyntaxElement): AST.Token;
301

302
  public getFirstToken(
303
    node: YAMLSyntaxElement,
304
    options?: CursorWithSkipOptions,
305
  ): YAMLToken | null;
306

307
  public getFirstToken(
308
    node: YAMLSyntaxElement,
309
    options?: CursorWithSkipOptions,
310
  ): YAMLToken | null {
311
    return this.tokenStore.getFirstToken(node, options);
31,905✔
312
  }
313

314
  public getLastToken(node: YAMLSyntaxElement): AST.Token;
315

316
  public getLastToken(
317
    node: YAMLSyntaxElement,
318
    options?: CursorWithSkipOptions,
319
  ): YAMLToken | null;
320

321
  public getLastToken(
322
    node: YAMLSyntaxElement,
323
    options?: CursorWithSkipOptions,
324
  ): YAMLToken | null {
325
    return this.tokenStore.getLastToken(node, options);
8,910✔
326
  }
327

328
  public getTokenBefore(node: YAMLSyntaxElement): AST.Token | null;
329

330
  public getTokenBefore(
331
    node: YAMLSyntaxElement,
332
    options?: CursorWithSkipOptions,
333
  ): YAMLToken | null;
334

335
  public getTokenBefore(
336
    node: YAMLSyntaxElement,
337
    options?: CursorWithSkipOptions,
338
  ): YAMLToken | null {
339
    return this.tokenStore.getTokenBefore(node, options);
22,077✔
340
  }
341

342
  public getTokensBefore(
343
    node: YAMLSyntaxElement,
344
    options?: CursorWithCountOptions,
345
  ): YAMLToken[] {
346
    return this.tokenStore.getTokensBefore(node, options);
96✔
347
  }
348

349
  public getTokenAfter(node: YAMLSyntaxElement): AST.Token | null;
350

351
  public getTokenAfter(
352
    node: YAMLSyntaxElement,
353
    options?: CursorWithSkipOptions,
354
  ): YAMLToken | null;
355

356
  public getTokenAfter(
357
    node: YAMLSyntaxElement,
358
    options?: CursorWithSkipOptions,
359
  ): YAMLToken | null {
360
    return this.tokenStore.getTokenAfter(node, options);
27,107✔
361
  }
362

363
  public getFirstTokenBetween(
364
    left: YAMLSyntaxElement,
365
    right: YAMLSyntaxElement,
366
    options?: CursorWithSkipOptions,
367
  ): YAMLToken | null {
368
    return this.tokenStore.getFirstTokenBetween(left, right, options);
×
369
  }
370

371
  public getTokensBetween(
372
    left: YAMLSyntaxElement,
373
    right: YAMLSyntaxElement,
374
    paddingOrOptions?: number | FilterPredicate | CursorWithCountOptions,
375
  ): YAMLToken[] {
376
    return this.tokenStore.getTokensBetween(left, right, paddingOrOptions);
249✔
377
  }
378

379
  public getTokens(
380
    node: AST.YAMLNode,
381
    options?: FilterPredicate | CursorWithCountOptions,
382
  ): YAMLToken[] {
383
    return this.tokenStore.getTokens(node, options);
1,561✔
384
  }
385

386
  public getCommentsBefore(nodeOrToken: YAMLSyntaxElement): AST.Comment[] {
387
    return this.tokenStore.getCommentsBefore(nodeOrToken);
110✔
388
  }
389

390
  public getCommentsAfter(nodeOrToken: YAMLSyntaxElement): AST.Comment[] {
391
    return this.tokenStore.getCommentsAfter(nodeOrToken);
2✔
392
  }
393

394
  public isSpaceBetween(first: YAMLToken, second: YAMLToken): boolean {
395
    if (nodesOrTokensOverlap(first, second)) {
683!
396
      return false;
×
397
    }
398

399
    const [startingNodeOrToken, endingNodeOrToken] =
400
      first.range[1] <= second.range[0] ? [first, second] : [second, first];
683!
401
    const firstToken =
402
      this.getLastToken(startingNodeOrToken) || startingNodeOrToken;
683!
403
    const finalToken =
404
      this.getFirstToken(endingNodeOrToken) || endingNodeOrToken;
683✔
405
    let currentToken: YAMLToken = firstToken;
683✔
406

407
    while (currentToken !== finalToken) {
683✔
408
      const nextToken: YAMLToken = this.getTokenAfter(currentToken, {
683✔
409
        includeComments: true,
410
      })!;
411

412
      if (currentToken.range[1] !== nextToken.range[0]) {
683✔
413
        return true;
312✔
414
      }
415

416
      currentToken = nextToken;
371✔
417
    }
418

419
    return false;
371✔
420
  }
421

422
  /**
423
   * Compatibility for ESLint's SourceCode API
424
   * @deprecated YAML does not have scopes
425
   */
426
  public getScope(node?: AST.YAMLNode): Scope.Scope | null {
427
    if (node?.type !== "Program") {
39!
428
      return null;
×
429
    }
430
    return createFakeGlobalScope(this.ast);
39✔
431
  }
432

433
  /**
434
   * Compatibility for ESLint's SourceCode API
435
   * @deprecated YAML does not have scopes
436
   */
437
  public get scopeManager(): Scope.ScopeManager | null {
438
    return {
×
439
      scopes: [],
440
      globalScope: createFakeGlobalScope(this.ast),
441
      acquire: (node) => {
442
        if (node.type === "Program") {
×
443
          return createFakeGlobalScope(this.ast);
×
444
        }
445
        return null;
×
446
      },
447
      getDeclaredVariables: () => [],
×
448
    };
449
  }
450

451
  /**
452
   * Compatibility for ESLint's SourceCode API
453
   * @deprecated
454
   */
455
  public isSpaceBetweenTokens(first: YAMLToken, second: YAMLToken): boolean {
UNCOV
456
    return this.isSpaceBetween(first, second);
×
457
  }
458

459
  private _getChildren(node: AST.YAMLNode) {
460
    const keys = this.visitorKeys[node.type] || [];
79!
461
    const children: AST.YAMLNode[] = [];
79✔
462
    for (const key of keys) {
79✔
463
      const value = (node as unknown as Record<string, unknown>)[key];
103✔
464
      if (Array.isArray(value)) {
103✔
465
        for (const element of value) {
55✔
466
          if (isNode(element)) {
61✔
467
            children.push(element);
61✔
468
          }
469
        }
470
      } else if (isNode(value)) {
48✔
471
        children.push(value);
48✔
472
      }
473
    }
474
    return children;
79✔
475
  }
476
}
477

478
/**
479
 * Determines whether the given value is a YAML AST node.
480
 */
481
function isNode(value: unknown): value is AST.YAMLNode {
1✔
482
  return (
109✔
483
    typeof value === "object" &&
654✔
484
    value !== null &&
485
    typeof (value as Record<string, unknown>).type === "string" &&
486
    Array.isArray((value as Record<string, unknown>).range) &&
487
    Boolean((value as Record<string, unknown>).loc) &&
488
    typeof (value as Record<string, unknown>).loc === "object"
489
  );
490
}
491

492
/**
493
 * Determines whether two nodes or tokens overlap.
494
 */
495
function nodesOrTokensOverlap(first: YAMLToken, second: YAMLToken): boolean {
1✔
496
  return first.range[0] < second.range[1] && second.range[0] < first.range[1];
683✔
497
}
498

499
/**
500
 * Creates a fake global scope for YAML files.
501
 * @deprecated YAML does not have scopes
502
 */
503
function createFakeGlobalScope(node: AST.YAMLProgram): Scope.Scope {
2!
504
  const fakeGlobalScope: Scope.Scope = {
39✔
505
    type: "global",
506
    block: node as never,
507
    set: new Map(),
508
    through: [],
509
    childScopes: [],
510
    variableScope: null as never,
511
    variables: [],
512
    references: [],
513
    functionExpressionScope: false,
514
    isStrict: false,
515
    upper: null,
516
    implicit: {
517
      variables: [],
518
      set: new Map(),
519
    },
520
  };
521
  fakeGlobalScope.variableScope = fakeGlobalScope;
39✔
522
  return fakeGlobalScope;
39✔
523
}
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