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

streetsidesoftware / cspell / 16236281234

12 Jul 2025 08:41AM UTC coverage: 92.382%. First build
16236281234

Pull #7598

github

web-flow
Merge 93fb91897 into be32f18dd
Pull Request #7598: fix: Make it easier to create config files.

13086 of 15458 branches covered (84.66%)

0 of 2 new or added lines in 1 file covered. (0.0%)

16019 of 17340 relevant lines covered (92.38%)

29718.12 hits per line

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

75.52
/packages/cspell-config-lib/src/CSpellConfigFile/CSpellConfigFileYaml.ts
1
import assert from 'node:assert';
2

3
import type { CSpellSettings } from '@cspell/cspell-types';
4
import {
5
    Document as YamlDocument,
6
    isAlias,
7
    isMap,
8
    isNode,
9
    isPair,
10
    isScalar,
11
    isSeq,
12
    type Node as YamlNode,
13
    type Pair,
14
    parseDocument,
15
    Scalar,
16
    stringify,
17
    visit as yamlWalkAst,
18
    YAMLMap,
19
    YAMLSeq,
20
} from 'yaml';
21

22
import { MutableCSpellConfigFile } from '../CSpellConfigFile.js';
23
import { detectIndentAsNum } from '../serializers/util.js';
24
import type { TextFile } from '../TextFile.js';
25
import type { KeyOf, ValueOf1 } from '../types.js';
26
import type {
27
    CfgArrayNode,
28
    CfgObjectNode,
29
    CfgScalarNode,
30
    NodeComments,
31
    NodeOrValue,
32
    NodeValue,
33
    RCfgNode,
34
} from '../UpdateConfig/CfgTree.js';
35
import { isNodeValue } from '../UpdateConfig/CfgTree.js';
36
import { ParseError } from './Errors.js';
37

38
type S = CSpellSettings;
39

40
export class CSpellConfigFileYaml extends MutableCSpellConfigFile {
41
    #settings: CSpellSettings | undefined = undefined;
26✔
42

43
    constructor(
44
        readonly url: URL,
26✔
45
        readonly yamlDoc: YamlDocument,
26✔
46
        readonly indent: number,
26✔
47
    ) {
48
        super(url);
26✔
49
        // Set the initial settings from the YAML document.
50
        this.#settings = this.yamlDoc.toJS() as CSpellSettings;
26✔
51
    }
52

53
    get settings(): CSpellSettings {
54
        return this.#settings ?? (this.yamlDoc.toJS() as CSpellSettings);
8✔
55
    }
56

57
    addWords(wordsToAdd: string[]): this {
58
        const cfgWords: YAMLSeq<StringOrScalar> =
59
            (this.yamlDoc.get('words') as YAMLSeq<StringOrScalar>) || new YAMLSeq<StringOrScalar>();
7!
60
        assert(isSeq(cfgWords), 'Expected words to be a YAML sequence');
7✔
61
        const knownWords = new Set(cfgWords.items.map((item) => getScalarValue(item)));
49✔
62
        wordsToAdd.forEach((w) => {
7✔
63
            if (knownWords.has(w)) return;
23✔
64
            cfgWords.add(w);
18✔
65
            knownWords.add(w);
18✔
66
        });
67
        const sorted = sortWords(cfgWords.items);
7✔
68
        sorted.forEach((item, index) => cfgWords.set(index, item));
67✔
69
        cfgWords.items.length = sorted.length;
7✔
70
        this.#setValue('words', cfgWords);
7✔
71
        this.#markAsMutable();
7✔
72
        return this;
7✔
73
    }
74

75
    serialize() {
76
        return stringify(this.yamlDoc, { indent: this.indent });
16✔
77
    }
78

79
    setValue<K extends keyof S>(key: K, value: NodeOrValue<ValueOf1<S, K>>): this {
80
        if (isNodeValue(value)) {
2✔
81
            let node = this.#getNode(key);
1✔
82
            if (!node) {
1!
83
                node = this.yamlDoc.createNode(value.value);
×
84
                setYamlNodeComments(node, value);
×
85
                this.#setValue(key, node);
×
86
            } else {
87
                setYamlNodeValue(node, value);
1✔
88
            }
89
        } else {
90
            this.#setValue(key, value);
1✔
91
        }
92
        this.#markAsMutable();
2✔
93
        return this;
2✔
94
    }
95

96
    getValue<K extends keyof S>(key: K): ValueOf1<S, K> {
97
        const node = this.#getNode(key);
4✔
98
        return node?.toJS(this.yamlDoc);
4✔
99
    }
100

101
    #getNode(key: unknown | unknown[]): YamlNode | undefined {
102
        return getYamlNode(this.yamlDoc, key);
25✔
103
    }
104

105
    getNode<K extends keyof S>(key: K): RCfgNode<ValueOf1<S, K>> | undefined;
106
    getNode<K extends keyof S>(
107
        key: K,
108
        defaultValue: Exclude<ValueOf1<S, K>, undefined>,
109
    ): Exclude<RCfgNode<ValueOf1<S, K>>, undefined>;
110
    getNode<K extends keyof S>(key: K, defaultValue: ValueOf1<S, K> | undefined): RCfgNode<ValueOf1<S, K>> | undefined;
111
    getNode<K extends keyof S>(
112
        key: K,
113
        defaultValue?: ValueOf1<CSpellSettings, K>,
114
    ): RCfgNode<ValueOf1<CSpellSettings, K>> | undefined {
115
        let yNode = this.#getNode(key);
20✔
116
        if (!yNode) {
20✔
117
            if (defaultValue === undefined) {
3✔
118
                return undefined;
2✔
119
            }
120
            yNode = this.yamlDoc.createNode(defaultValue);
1✔
121
            this.#setValue(key, yNode);
1✔
122
        }
123
        this.#markAsMutable();
18✔
124
        return toConfigNode(this.yamlDoc, yNode) as RCfgNode<ValueOf1<CSpellSettings, K>>;
18✔
125
    }
126

127
    getFieldNode<K extends keyof S>(key: K): RCfgNode<string> | undefined {
128
        const contents = this.yamlDoc.contents;
6✔
129
        if (!isMap(contents)) {
6!
130
            return undefined;
×
131
        }
132

133
        const found = findPair(contents, key as string);
6✔
134
        const pair = found && this.#fixPair(found);
6✔
135
        if (!pair) {
6!
136
            return undefined;
×
137
        }
138
        return toConfigNode(this.yamlDoc, pair.key) as RCfgNode<string>;
6✔
139
    }
140

141
    /**
142
     * Removes a value from the document.
143
     * @returns `true` if the item was found and removed.
144
     */
145
    delete(key: keyof S): boolean {
146
        const removed = this.yamlDoc.delete(key);
1✔
147
        if (removed) {
1!
148
            this.#markAsMutable();
1✔
149
        }
150
        return removed;
1✔
151
    }
152

153
    get comment(): string | undefined {
154
        return this.yamlDoc.comment ?? undefined;
×
155
    }
156

157
    set comment(comment: string | undefined) {
158
        // eslint-disable-next-line unicorn/no-null
159
        this.yamlDoc.comment = comment ?? null;
×
160
    }
161

162
    setSchema(schemaRef: string): this {
163
        removeSchemaComment(this.yamlDoc);
2✔
164
        let commentBefore = this.yamlDoc.commentBefore || '';
2✔
165
        commentBefore = commentBefore.replace(/^ yaml-language-server: \$schema=.*\n?/m, '');
2✔
166
        commentBefore = ` yaml-language-server: $schema=${schemaRef}` + (commentBefore ? '\n' + commentBefore : '');
2!
167
        this.yamlDoc.commentBefore = commentBefore;
2✔
168

169
        // Remove any existing comment references that might be attached to the first field.
170
        const contents = this.#getContentsMap();
2✔
171
        const firstPair = contents.items[0];
2✔
172
        if (firstPair && isPair(firstPair)) {
2!
173
            const key = firstPair.key;
2✔
174
            if (isNode(key)) {
2!
175
                removeSchemaComment(key);
2✔
176
            }
177
        }
178

179
        if (this.getNode('$schema')) {
2!
180
            this.setValue('$schema', schemaRef);
×
181
        }
182
        return this;
2✔
183
    }
184

185
    removeAllComments(): this {
186
        const doc = this.yamlDoc;
1✔
187
        // eslint-disable-next-line unicorn/no-null
188
        doc.comment = null;
1✔
189
        // eslint-disable-next-line unicorn/no-null
190
        doc.commentBefore = null;
1✔
191
        yamlWalkAst(this.yamlDoc, (_, node) => {
1✔
192
            if (!(isScalar(node) || isMap(node) || isSeq(node))) return;
18✔
193
            // eslint-disable-next-line unicorn/no-null
194
            node.comment = null;
14✔
195
            // eslint-disable-next-line unicorn/no-null
196
            node.commentBefore = null;
14✔
197
        });
198
        return this;
1✔
199
    }
200

201
    setComment(key: keyof CSpellSettings, comment: string, inline?: boolean): this {
202
        const node = this.getFieldNode(key);
4✔
203
        if (!node) return this;
4!
204

205
        if (inline) {
4✔
206
            node.comment = comment;
2✔
207
        } else {
208
            node.commentBefore = comment;
2✔
209
        }
210

211
        return this;
4✔
212
    }
213

214
    /**
215
     * Marks the config file as mutable. Any access to settings will the settings to be regenerated
216
     * from the YAML document.
217
     */
218
    #markAsMutable() {
219
        this.#settings = undefined;
28✔
220
    }
221

222
    #setValue(key: string | Scalar<string>, value: unknown | YamlNode): void {
223
        this.yamlDoc.set(key, value);
9✔
224
        const contents = this.#getContentsMap();
9✔
225
        const pair = findPair(contents, key);
9✔
226
        assert(pair, `Expected pair for key: ${String(key)}`);
9✔
227
        this.#fixPair(pair);
9✔
228
    }
229

230
    #toNode<T>(value: T | YamlNode): YamlNode<T> {
231
        return (isNode(value) ? value : this.yamlDoc.createNode(value)) as YamlNode<T>;
30✔
232
    }
233

234
    #fixPair(pair: Pair): Pair<Scalar<string>, YamlNode> | undefined {
235
        assert(isPair(pair), 'Expected pair to be a Pair');
15✔
236
        pair.key = this.#toNode(pair.key);
15✔
237
        pair.value = this.#toNode(pair.value);
15✔
238
        return pair as Pair<Scalar<string>, YamlNode>;
15✔
239
    }
240

241
    #getContentsMap(): YAMLMap {
242
        const contents = this.yamlDoc.contents;
11✔
243
        assert(isMap(contents), 'Expected contents to be a YAMLMap');
11✔
244
        return contents as YAMLMap;
11✔
245
    }
246

247
    static parse(file: TextFile): CSpellConfigFileYaml {
248
        return parseCSpellConfigFileYaml(file);
1✔
249
    }
250
}
251

252
export function createCSpellConfigFileYaml(url: URL, settings: CSpellSettings, indent = 2): CSpellConfigFileYaml {
×
NEW
253
    const yamlDoc = new YamlDocument(settings);
×
NEW
254
    return new CSpellConfigFileYaml(url, yamlDoc, indent);
×
255
}
256

257
export function parseCSpellConfigFileYaml(file: TextFile): CSpellConfigFileYaml {
258
    const { url, content } = file;
29✔
259

260
    try {
29✔
261
        const doc = parseDocument<YAMLMap | Scalar<null | string>>(content);
29✔
262
        // Force empty content to be a map.
263
        if (doc.contents === null || (isScalar(doc.contents) && !doc.contents.value)) {
29✔
264
            doc.contents = new YAMLMap();
3✔
265
        }
266
        if (!isMap(doc.contents)) {
29✔
267
            throw new ParseError(url, `Invalid YAML content ${url}`);
3✔
268
        }
269
        const indent = detectIndentAsNum(content);
26✔
270
        return new CSpellConfigFileYaml(url, doc, indent);
26✔
271
    } catch (e) {
272
        if (e instanceof ParseError) {
3!
273
            throw e;
3✔
274
        }
275
        throw new ParseError(url, undefined, { cause: e });
×
276
    }
277
}
278

279
function getScalarValue<T>(node: T | Scalar<T>): T {
280
    if (isScalar(node)) {
259✔
281
        return node.value;
196✔
282
    }
283
    return node;
63✔
284
}
285

286
function toScalar<T>(node: T | Scalar<T>): Scalar<T> {
287
    if (isScalar(node)) {
78✔
288
        return node;
60✔
289
    }
290
    return new Scalar(node);
18✔
291
}
292

293
type StringOrScalar = string | Scalar<string>;
294

295
function groupWords(words: StringOrScalar[]): StringOrScalar[][] {
296
    const groups: StringOrScalar[][] = [];
7✔
297
    if (words.length === 0) {
7!
298
        return groups;
×
299
    }
300
    let currentGroup: StringOrScalar[] = [];
7✔
301
    groups.push(currentGroup);
7✔
302
    for (const word of words) {
7✔
303
        if (isSectionHeader(word)) {
67✔
304
            currentGroup = [];
8✔
305
            groups.push(currentGroup);
8✔
306
        }
307
        currentGroup.push(cloneWord(word));
67✔
308
    }
309
    return groups;
7✔
310
}
311

312
function isSectionHeader(word: StringOrScalar): boolean {
313
    if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false;
67✔
314
    if (word.spaceBefore) return true;
16✔
315
    if (!word.commentBefore) return false;
8!
316
    return word.commentBefore.includes('\n\n');
8✔
317
}
318

319
function adjustSectionHeader(word: Scalar<string>, prev: StringOrScalar, isFirstSection: boolean): void {
320
    // console.log('adjustSectionHeader %o', { word, prev, isFirstSection });
321
    if (!isScalar(prev)) return;
11!
322
    let captureComment = isFirstSection;
11✔
323
    if (prev.spaceBefore) {
11✔
324
        word.spaceBefore = true;
6✔
325
        captureComment = true;
6✔
326
        delete prev.spaceBefore;
6✔
327
    }
328
    if (!prev.commentBefore) return;
11✔
329

330
    const originalComment = prev.commentBefore;
5✔
331
    const lines = originalComment.split(/^\n/gm);
5✔
332
    const lastLine = lines[lines.length - 1];
5✔
333
    // console.log('adjustSectionHeader lines %o', { lines, isFirstSection, lastLine, originalComment });
334
    captureComment = (captureComment && originalComment.trim() === lastLine.trim()) || originalComment.endsWith('\n');
5!
335
    let header = originalComment;
5✔
336
    if (captureComment) {
5✔
337
        delete prev.commentBefore;
3✔
338
    } else {
339
        prev.commentBefore = lastLine;
2✔
340
        lines.pop();
2✔
341
        header = lines.join('\n');
2✔
342
    }
343
    if (word.commentBefore) {
5✔
344
        header += header.endsWith('\n\n') ? '' : '\n';
2!
345
        header += header.endsWith('\n\n') ? '' : '\n';
2✔
346
        header += word.commentBefore;
2✔
347
    }
348
    word.commentBefore = header;
5✔
349
    // console.log('adjustSectionHeader after %o', { word, prev, isFirstSection, originalComment, lastLine, lines });
350
}
351

352
function sortWords(words: StringOrScalar[]): StringOrScalar[] {
353
    const compare = new Intl.Collator().compare;
7✔
354

355
    const groups = groupWords(words);
7✔
356
    let firstGroup = true;
7✔
357
    for (const group of groups) {
7✔
358
        const head = group[0];
15✔
359
        group.sort((a, b) => {
15✔
360
            return compare(getScalarValue(a), getScalarValue(b));
105✔
361
        });
362
        if (group[0] !== head && isScalar(head)) {
15✔
363
            const first = (group[0] = toScalar(group[0]));
11✔
364
            adjustSectionHeader(first, head, firstGroup);
11✔
365
        }
366
        firstGroup = false;
15✔
367
    }
368

369
    const result = groups.flat();
7✔
370
    return result.map((w) => toScalar(w));
67✔
371
}
372

373
function cloneWord(word: StringOrScalar): StringOrScalar {
374
    if (isScalar(word)) {
67✔
375
        return word.clone() as Scalar<string>;
49✔
376
    }
377
    return word;
18✔
378
}
379

380
function getYamlNode(yamlDoc: YamlDocument | YAMLMap | YAMLSeq, key: unknown | unknown[]): YamlNode | undefined {
381
    return (Array.isArray(key) ? yamlDoc.getIn(key, true) : yamlDoc.get(key, true)) as YamlNode | undefined;
30!
382
}
383

384
type ArrayType<T> = T extends unknown[] ? T[number] : never;
385

386
function toConfigNode<T>(doc: YamlDocument, yNode: YamlNode): RCfgNode<T> {
387
    if (isYamlSeq(yNode)) {
28✔
388
        return toConfigArrayNode(doc, yNode) as RCfgNode<T>;
10✔
389
    }
390
    if (isMap(yNode)) {
18✔
391
        return toConfigObjectNode(doc, yNode) as RCfgNode<T>;
1✔
392
    }
393
    if (isScalar(yNode)) {
17!
394
        return toConfigScalarNode(doc, yNode) as RCfgNode<T>;
17✔
395
    }
396
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yNode)}`);
×
397
}
398

399
abstract class ConfigNodeBase<N extends 'array' | 'object' | 'scalar', T> {
400
    constructor(readonly type: N) {}
28✔
401

402
    abstract value: T;
403
    abstract comment: string | undefined;
404
    abstract commentBefore: string | undefined;
405
}
406

407
class ConfigArrayNode<T extends unknown[]>
408
    extends ConfigNodeBase<'array', ArrayType<T>[]>
409
    implements CfgArrayNode<ArrayType<T>>
410
{
411
    #doc: YamlDocument;
412
    #yNode: YAMLSeq<ArrayType<T>>;
413

414
    constructor(doc: YamlDocument, yNode: YAMLSeq<ArrayType<T>>) {
415
        super('array');
10✔
416
        this.#doc = doc;
10✔
417
        this.#yNode = yNode;
10✔
418
    }
419

420
    get value(): ArrayType<T>[] {
421
        return this.#yNode.toJS(this.#doc) as ArrayType<T>[];
×
422
    }
423
    get comment() {
424
        return this.#yNode.comment ?? undefined;
1✔
425
    }
426
    set comment(comment: string | undefined) {
427
        // eslint-disable-next-line unicorn/no-null
428
        this.#yNode.comment = comment ?? null;
×
429
    }
430
    get commentBefore() {
431
        return this.#yNode.commentBefore ?? undefined;
1!
432
    }
433
    set commentBefore(comment: string | undefined) {
434
        // eslint-disable-next-line unicorn/no-null
435
        this.#yNode.commentBefore = comment ?? null;
×
436
    }
437

438
    getNode(key: number) {
439
        const node = getYamlNode(this.#yNode, key);
4✔
440
        if (!node) return undefined;
4!
441
        return toConfigNode<ArrayType<T>>(this.#doc, node) as RCfgNode<ArrayType<T>>;
4✔
442
    }
443

444
    getValue(key: number): ArrayType<T> | undefined {
445
        const node = getYamlNode(this.#yNode, key);
1✔
446
        if (!node) return undefined;
1!
447
        return node.toJS(this.#doc) as ArrayType<T>;
1✔
448
    }
449

450
    setValue(key: number, value: NodeOrValue<ArrayType<T>>): void {
451
        if (!isNodeValue(value)) {
1!
452
            this.#yNode.set(key, value);
1✔
453
            return;
1✔
454
        }
455
        this.#yNode.set(key, value.value);
×
456
        const yNodeValue = getYamlNode(this.#yNode, key);
×
457
        assert(yNodeValue);
×
458
        // eslint-disable-next-line unicorn/no-null
459
        yNodeValue.comment = value.comment ?? null;
×
460
        // eslint-disable-next-line unicorn/no-null
461
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
462
    }
463

464
    delete(key: number): boolean {
465
        return this.#yNode.delete(key);
×
466
    }
467

468
    push(value: NodeOrValue<ArrayType<T>>): number {
469
        if (!isNodeValue(value)) {
×
470
            this.#yNode.add(value);
×
471
            return this.#yNode.items.length;
×
472
        }
473
        this.#yNode.add(value.value);
×
474

475
        setYamlNodeComments(getYamlNode(this.#yNode, this.#yNode.items.length - 1), value);
×
476
        return this.#yNode.items.length;
×
477
    }
478

479
    get length(): number {
480
        return this.#yNode.items.length;
×
481
    }
482
}
483

484
function toConfigArrayNode<T extends unknown[]>(doc: YamlDocument, yNode: YAMLSeq): CfgArrayNode<ArrayType<T>> {
485
    return new ConfigArrayNode<T>(doc, yNode as YAMLSeq<ArrayType<T>>);
10✔
486
}
487

488
class ConfigObjectNode<T extends object> extends ConfigNodeBase<'object', T> implements CfgObjectNode<T> {
489
    #doc: YamlDocument;
490
    #yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>;
491

492
    constructor(doc: YamlDocument, yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>) {
493
        super('object');
1✔
494
        this.#doc = doc;
1✔
495
        this.#yNode = yNode;
1✔
496
    }
497

498
    get value(): T {
499
        return this.#yNode.toJS(this.#doc) as T;
×
500
    }
501
    get comment() {
502
        return this.#yNode.comment ?? undefined;
×
503
    }
504
    set comment(comment: string | undefined) {
505
        // eslint-disable-next-line unicorn/no-null
506
        this.#yNode.comment = comment ?? null;
×
507
    }
508
    get commentBefore() {
509
        return this.#yNode.commentBefore ?? undefined;
×
510
    }
511
    set commentBefore(comment: string | undefined) {
512
        // eslint-disable-next-line unicorn/no-null
513
        this.#yNode.commentBefore = comment ?? null;
×
514
    }
515

516
    getValue<K extends keyof T>(key: K): T[K] | undefined {
517
        const node = getYamlNode(this.#yNode, key);
×
518
        if (!node) return undefined;
×
519
        return node.toJS(this.#doc) as T[K];
×
520
    }
521
    getNode<K extends keyof T>(key: K): RCfgNode<T[K]> | undefined {
522
        const node = getYamlNode(this.#yNode, key);
×
523
        if (!node) return undefined;
×
524
        return toConfigNode<T[K]>(this.#doc, node);
×
525
    }
526
    setValue<K extends KeyOf<T>>(key: K, value: NodeOrValue<ValueOf1<T, K>>): void {
527
        if (!isNodeValue(value)) {
×
528
            this.#yNode.set(key, value);
×
529
            return;
×
530
        }
531
        this.#yNode.set(key, value.value);
×
532
        const yNodeValue = getYamlNode(this.#yNode, key);
×
533
        assert(yNodeValue);
×
534
        // eslint-disable-next-line unicorn/no-null
535
        yNodeValue.comment = value.comment ?? null;
×
536
        // eslint-disable-next-line unicorn/no-null
537
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
538
    }
539
    delete<K extends KeyOf<T>>(key: K): boolean {
540
        return this.#yNode.delete(key);
×
541
    }
542
}
543

544
function toConfigObjectNode<T extends object>(doc: YamlDocument, yNode: YAMLMap): CfgObjectNode<T> {
545
    return new ConfigObjectNode<T>(doc, yNode as YAMLMap<KeyOf<T>, T[KeyOf<T>]>);
1✔
546
}
547

548
class ConfigScalarNode<T extends string | number | boolean | null | undefined>
549
    extends ConfigNodeBase<'scalar', T>
550
    implements CfgScalarNode<T>
551
{
552
    private $doc: YamlDocument;
553
    private $yNode: Scalar<T>;
554

555
    readonly type = 'scalar';
17✔
556

557
    constructor(doc: YamlDocument, yNode: Scalar<T>) {
558
        super('scalar');
17✔
559
        this.$doc = doc;
17✔
560
        this.$yNode = yNode;
17✔
561
        assert(isScalar(yNode), 'Expected yNode to be a Scalar');
17✔
562
    }
563

564
    get value() {
565
        return this.$yNode.toJS(this.$doc) as T;
5✔
566
    }
567

568
    set value(value: T) {
569
        this.$yNode.value = value;
×
570
    }
571

572
    get comment() {
573
        return this.$yNode.comment ?? undefined;
10✔
574
    }
575

576
    set comment(comment: string | undefined) {
577
        // eslint-disable-next-line unicorn/no-null
578
        this.$yNode.comment = comment ?? null;
2!
579
    }
580

581
    get commentBefore() {
582
        return this.$yNode.commentBefore ?? undefined;
10✔
583
    }
584

585
    set commentBefore(comment: string | undefined) {
586
        // eslint-disable-next-line unicorn/no-null
587
        this.$yNode.commentBefore = comment ?? null;
2!
588
    }
589

590
    toJSON() {
591
        return {
×
592
            type: this.type,
593
            value: this.value,
594
            comment: this.comment,
595
            commentBefore: this.commentBefore,
596
        };
597
    }
598
}
599

600
function toConfigScalarNode<T extends string | number | boolean | null | undefined>(
601
    doc: YamlDocument,
602
    yNode: Scalar,
603
): CfgScalarNode<T> {
604
    return new ConfigScalarNode<T>(doc, yNode as Scalar<T>);
17✔
605
}
606

607
function isYamlSeq<T>(node: YamlNode): node is YAMLSeq<T> {
608
    return isSeq(node);
28✔
609
}
610

611
function yamlNodeType(node: YamlNode): 'scalar' | 'seq' | 'map' | 'alias' | 'unknown' {
612
    if (isScalar(node)) return 'scalar';
×
613
    if (isSeq(node)) return 'seq';
×
614
    if (isMap(node)) return 'map';
×
615
    if (isAlias(node)) return 'alias';
×
616
    return 'unknown';
×
617
}
618

619
function setYamlNodeComments(yamlNode: YamlNode | undefined, comments: NodeComments): void {
620
    if (!yamlNode) return;
1!
621
    if ('comment' in comments) {
1!
622
        // eslint-disable-next-line unicorn/no-null
623
        yamlNode.comment = comments.comment ?? null;
1!
624
    }
625
    if ('commentBefore' in comments) {
1!
626
        // eslint-disable-next-line unicorn/no-null
627
        yamlNode.commentBefore = comments.commentBefore ?? null;
1!
628
    }
629
}
630

631
function setYamlNodeValue<T>(yamlNode: YamlNode, nodeValue: NodeValue<T>): void {
632
    setYamlNodeComments(yamlNode, nodeValue);
1✔
633
    if (isScalar(yamlNode)) {
1!
634
        yamlNode.value = nodeValue.value;
1✔
635
        return;
1✔
636
    }
637
    const value = nodeValue.value;
×
638
    if (isSeq(yamlNode)) {
×
639
        assert(Array.isArray(value), 'Expected value to be an array for YAMLSeq');
×
640
        yamlNode.items = [];
×
641
        for (let i = 0; i < value.length; ++i) {
×
642
            yamlNode.set(i, value[i]);
×
643
        }
644
        return;
×
645
    }
646
    if (isMap(yamlNode)) {
×
647
        assert(typeof value === 'object' && value !== null, 'Expected value to be an object for YAMLMap');
×
648
        yamlNode.items = [];
×
649
        for (const [key, val] of Object.entries(value)) {
×
650
            yamlNode.set(key, val);
×
651
        }
652
        return;
×
653
    }
654
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yamlNode)}`);
×
655
}
656

657
function findPair(yNode: YamlNode, yKey: string | Scalar<string>): Pair<Scalar<string> | string, YamlNode> | undefined {
658
    const key = isScalar(yKey) ? yKey.value : yKey;
15!
659
    if (!isMap(yNode)) return undefined;
15!
660
    const items = yNode.items as Pair<YamlNode | string, YamlNode>[];
15✔
661
    for (const item of items) {
15✔
662
        if (!isPair(item)) continue;
41!
663
        if (item.key === key) {
41✔
664
            return item as Pair<string, YamlNode>;
1✔
665
        }
666
        if (isScalar(item.key) && item.key.value === key) {
40✔
667
            return item as Pair<Scalar<string>, YamlNode>;
14✔
668
        }
669
    }
670
    return undefined;
×
671
}
672

673
function removeSchemaComment(node: { commentBefore?: string | null }): void {
674
    if (!node.commentBefore) return;
4✔
675
    // eslint-disable-next-line unicorn/no-null
676
    node.commentBefore = node.commentBefore?.replace(/^ yaml-language-server: \$schema=.*\n?/gm, '') ?? null;
1!
677
}
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