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

streetsidesoftware / cspell / 15428360260

03 Jun 2025 09:35PM UTC coverage: 92.362% (-0.8%) from 93.187%
15428360260

Pull #7414

github

web-flow
Merge b37bef57d into 6e3c5d0e0
Pull Request #7414: fix: Add init command to command-line.

12857 of 15175 branches covered (84.72%)

210 of 382 new or added lines in 13 files covered. (54.97%)

2 existing lines in 1 file now uncovered.

15828 of 17137 relevant lines covered (92.36%)

30059.62 hits per line

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

75.09
/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
    type 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!
NEW
83
                node = this.yamlDoc.createNode(value.value);
×
NEW
84
                setYamlNodeComments(node, value);
×
NEW
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!
NEW
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!
NEW
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 {
NEW
154
        return this.yamlDoc.comment ?? undefined;
×
155
    }
156

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

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

173
    removeAllComments(): this {
174
        const doc = this.yamlDoc;
1✔
175
        // eslint-disable-next-line unicorn/no-null
176
        doc.comment = null;
1✔
177
        // eslint-disable-next-line unicorn/no-null
178
        doc.commentBefore = null;
1✔
179
        yamlWalkAst(this.yamlDoc, (_, node) => {
1✔
180
            if (!(isScalar(node) || isMap(node) || isSeq(node))) return;
18✔
181
            // eslint-disable-next-line unicorn/no-null
182
            node.comment = null;
14✔
183
            // eslint-disable-next-line unicorn/no-null
184
            node.commentBefore = null;
14✔
185
        });
186
        return this;
1✔
187
    }
188

189
    setComment(key: keyof CSpellSettings, comment: string, inline?: boolean): this {
190
        const node = this.getFieldNode(key);
4✔
191
        if (!node) return this;
4!
192

193
        if (inline) {
4✔
194
            node.comment = comment;
2✔
195
        } else {
196
            node.commentBefore = comment;
2✔
197
        }
198

199
        return this;
4✔
200
    }
201

202
    /**
203
     * Marks the config file as mutable. Any access to settings will the settings to be regenerated
204
     * from the YAML document.
205
     */
206
    #markAsMutable() {
207
        this.#settings = undefined;
28✔
208
    }
209

210
    #setValue(key: string | Scalar<string>, value: unknown | YamlNode): void {
211
        this.yamlDoc.set(key, value);
9✔
212
        const contents = this.yamlDoc.contents;
9✔
213
        assert(isMap(contents), 'Expected contents to be a YAMLMap');
9✔
214
        const pair = findPair(contents, key);
9✔
215
        assert(pair, `Expected pair for key: ${String(key)}`);
9✔
216
        this.#fixPair(pair);
9✔
217
    }
218

219
    #toNode<T>(value: T | YamlNode): YamlNode<T> {
220
        return (isNode(value) ? value : this.yamlDoc.createNode(value)) as YamlNode<T>;
30✔
221
    }
222

223
    #fixPair(pair: Pair<YamlNode | string, YamlNode>): Pair<Scalar<string>, YamlNode> | undefined {
224
        assert(isPair(pair), 'Expected pair to be a Pair');
15✔
225
        pair.key = this.#toNode(pair.key);
15✔
226
        pair.value = this.#toNode(pair.value);
15✔
227
        return pair as Pair<Scalar<string>, YamlNode>;
15✔
228
    }
229

230
    static parse(file: TextFile): CSpellConfigFileYaml {
231
        return parseCSpellConfigFileYaml(file);
1✔
232
    }
233
}
234

235
export function parseCSpellConfigFileYaml(file: TextFile): CSpellConfigFileYaml {
236
    const { url, content } = file;
29✔
237

238
    try {
29✔
239
        const doc = parseDocument<YAMLMap | Scalar<null | string>>(content);
29✔
240
        // Force empty content to be a map.
241
        if (doc.contents === null || (isScalar(doc.contents) && !doc.contents.value)) {
29✔
242
            doc.contents = new YAMLMap();
3✔
243
        }
244
        if (!isMap(doc.contents)) {
29✔
245
            throw new ParseError(url, `Invalid YAML content ${url}`);
3✔
246
        }
247
        const indent = detectIndentAsNum(content);
26✔
248
        return new CSpellConfigFileYaml(url, doc, indent);
26✔
249
    } catch (e) {
250
        if (e instanceof ParseError) {
3!
251
            throw e;
3✔
252
        }
UNCOV
253
        throw new ParseError(url, undefined, { cause: e });
×
254
    }
255
}
256

257
function getScalarValue<T>(node: T | Scalar<T>): T {
258
    if (isScalar(node)) {
259✔
259
        return node.value;
196✔
260
    }
261
    return node;
63✔
262
}
263

264
function toScalar<T>(node: T | Scalar<T>): Scalar<T> {
265
    if (isScalar(node)) {
78✔
266
        return node;
60✔
267
    }
268
    return new Scalar(node);
18✔
269
}
270

271
type StringOrScalar = string | Scalar<string>;
272

273
function groupWords(words: StringOrScalar[]): StringOrScalar[][] {
274
    const groups: StringOrScalar[][] = [];
7✔
275
    if (words.length === 0) {
7!
UNCOV
276
        return groups;
×
277
    }
278
    let currentGroup: StringOrScalar[] = [];
7✔
279
    groups.push(currentGroup);
7✔
280
    for (const word of words) {
7✔
281
        if (isSectionHeader(word)) {
67✔
282
            currentGroup = [];
8✔
283
            groups.push(currentGroup);
8✔
284
        }
285
        currentGroup.push(cloneWord(word));
67✔
286
    }
287
    return groups;
7✔
288
}
289

290
function isSectionHeader(word: StringOrScalar): boolean {
291
    if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false;
67✔
292
    if (word.spaceBefore) return true;
16✔
293
    if (!word.commentBefore) return false;
8!
294
    return word.commentBefore.includes('\n\n');
8✔
295
}
296

297
function adjustSectionHeader(word: Scalar<string>, prev: StringOrScalar, isFirstSection: boolean): void {
298
    // console.log('adjustSectionHeader %o', { word, prev, isFirstSection });
299
    if (!isScalar(prev)) return;
11!
300
    let captureComment = isFirstSection;
11✔
301
    if (prev.spaceBefore) {
11✔
302
        word.spaceBefore = true;
6✔
303
        captureComment = true;
6✔
304
        delete prev.spaceBefore;
6✔
305
    }
306
    if (!prev.commentBefore) return;
11✔
307

308
    const originalComment = prev.commentBefore;
5✔
309
    const lines = originalComment.split(/^\n/gm);
5✔
310
    const lastLine = lines[lines.length - 1];
5✔
311
    // console.log('adjustSectionHeader lines %o', { lines, isFirstSection, lastLine, originalComment });
312
    captureComment = (captureComment && originalComment.trim() === lastLine.trim()) || originalComment.endsWith('\n');
5!
313
    let header = originalComment;
5✔
314
    if (captureComment) {
5✔
315
        delete prev.commentBefore;
3✔
316
    } else {
317
        prev.commentBefore = lastLine;
2✔
318
        lines.pop();
2✔
319
        header = lines.join('\n');
2✔
320
    }
321
    if (word.commentBefore) {
5✔
322
        header += header.endsWith('\n\n') ? '' : '\n';
2!
323
        header += header.endsWith('\n\n') ? '' : '\n';
2✔
324
        header += word.commentBefore;
2✔
325
    }
326
    word.commentBefore = header;
5✔
327
    // console.log('adjustSectionHeader after %o', { word, prev, isFirstSection, originalComment, lastLine, lines });
328
}
329

330
function sortWords(words: StringOrScalar[]): StringOrScalar[] {
331
    const compare = new Intl.Collator().compare;
7✔
332

333
    const groups = groupWords(words);
7✔
334
    let firstGroup = true;
7✔
335
    for (const group of groups) {
7✔
336
        const head = group[0];
15✔
337
        group.sort((a, b) => {
15✔
338
            return compare(getScalarValue(a), getScalarValue(b));
105✔
339
        });
340
        if (group[0] !== head && isScalar(head)) {
15✔
341
            const first = (group[0] = toScalar(group[0]));
11✔
342
            adjustSectionHeader(first, head, firstGroup);
11✔
343
        }
344
        firstGroup = false;
15✔
345
    }
346

347
    const result = groups.flat();
7✔
348
    return result.map((w) => toScalar(w));
67✔
349
}
350

351
function cloneWord(word: StringOrScalar): StringOrScalar {
352
    if (isScalar(word)) {
67✔
353
        return word.clone() as Scalar<string>;
49✔
354
    }
355
    return word;
18✔
356
}
357

358
function getYamlNode(yamlDoc: YamlDocument | YAMLMap | YAMLSeq, key: unknown | unknown[]): YamlNode | undefined {
359
    return (Array.isArray(key) ? yamlDoc.getIn(key, true) : yamlDoc.get(key, true)) as YamlNode | undefined;
30!
360
}
361

362
type ArrayType<T> = T extends unknown[] ? T[number] : never;
363

364
function toConfigNode<T>(doc: YamlDocument, yNode: YamlNode): RCfgNode<T> {
365
    if (isYamlSeq(yNode)) {
28✔
366
        return toConfigArrayNode(doc, yNode) as RCfgNode<T>;
10✔
367
    }
368
    if (isMap(yNode)) {
18✔
369
        return toConfigObjectNode(doc, yNode) as RCfgNode<T>;
1✔
370
    }
371
    if (isScalar(yNode)) {
17!
372
        return toConfigScalarNode(doc, yNode) as RCfgNode<T>;
17✔
373
    }
NEW
374
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yNode)}`);
×
375
}
376

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

380
    abstract value: T;
381
    abstract comment: string | undefined;
382
    abstract commentBefore: string | undefined;
383
}
384

385
class ConfigArrayNode<T extends unknown[]>
386
    extends ConfigNodeBase<'array', ArrayType<T>[]>
387
    implements CfgArrayNode<ArrayType<T>>
388
{
389
    #doc: YamlDocument;
390
    #yNode: YAMLSeq<ArrayType<T>>;
391

392
    constructor(doc: YamlDocument, yNode: YAMLSeq<ArrayType<T>>) {
393
        super('array');
10✔
394
        this.#doc = doc;
10✔
395
        this.#yNode = yNode;
10✔
396
    }
397

398
    get value(): ArrayType<T>[] {
NEW
399
        return this.#yNode.toJS(this.#doc) as ArrayType<T>[];
×
400
    }
401
    get comment() {
402
        return this.#yNode.comment ?? undefined;
1✔
403
    }
404
    set comment(comment: string | undefined) {
405
        // eslint-disable-next-line unicorn/no-null
NEW
406
        this.#yNode.comment = comment ?? null;
×
407
    }
408
    get commentBefore() {
409
        return this.#yNode.commentBefore ?? undefined;
1!
410
    }
411
    set commentBefore(comment: string | undefined) {
412
        // eslint-disable-next-line unicorn/no-null
NEW
413
        this.#yNode.commentBefore = comment ?? null;
×
414
    }
415

416
    getNode(key: number) {
417
        const node = getYamlNode(this.#yNode, key);
4✔
418
        if (!node) return undefined;
4!
419
        return toConfigNode<ArrayType<T>>(this.#doc, node) as RCfgNode<ArrayType<T>>;
4✔
420
    }
421

422
    getValue(key: number): ArrayType<T> | undefined {
423
        const node = getYamlNode(this.#yNode, key);
1✔
424
        if (!node) return undefined;
1!
425
        return node.toJS(this.#doc) as ArrayType<T>;
1✔
426
    }
427

428
    setValue(key: number, value: NodeOrValue<ArrayType<T>>): void {
429
        if (!isNodeValue(value)) {
1!
430
            this.#yNode.set(key, value);
1✔
431
            return;
1✔
432
        }
NEW
433
        this.#yNode.set(key, value.value);
×
NEW
434
        const yNodeValue = getYamlNode(this.#yNode, key);
×
NEW
435
        assert(yNodeValue);
×
436
        // eslint-disable-next-line unicorn/no-null
NEW
437
        yNodeValue.comment = value.comment ?? null;
×
438
        // eslint-disable-next-line unicorn/no-null
NEW
439
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
440
    }
441

442
    delete(key: number): boolean {
NEW
443
        return this.#yNode.delete(key);
×
444
    }
445

446
    push(value: NodeOrValue<ArrayType<T>>): number {
NEW
447
        if (!isNodeValue(value)) {
×
NEW
448
            this.#yNode.add(value);
×
NEW
449
            return this.#yNode.items.length;
×
450
        }
NEW
451
        this.#yNode.add(value.value);
×
452

NEW
453
        setYamlNodeComments(getYamlNode(this.#yNode, this.#yNode.items.length - 1), value);
×
NEW
454
        return this.#yNode.items.length;
×
455
    }
456

457
    get length(): number {
NEW
458
        return this.#yNode.items.length;
×
459
    }
460
}
461

462
function toConfigArrayNode<T extends unknown[]>(doc: YamlDocument, yNode: YAMLSeq): CfgArrayNode<ArrayType<T>> {
463
    return new ConfigArrayNode<T>(doc, yNode as YAMLSeq<ArrayType<T>>);
10✔
464
}
465

466
class ConfigObjectNode<T extends object> extends ConfigNodeBase<'object', T> implements CfgObjectNode<T> {
467
    #doc: YamlDocument;
468
    #yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>;
469

470
    constructor(doc: YamlDocument, yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>) {
471
        super('object');
1✔
472
        this.#doc = doc;
1✔
473
        this.#yNode = yNode;
1✔
474
    }
475

476
    get value(): T {
NEW
477
        return this.#yNode.toJS(this.#doc) as T;
×
478
    }
479
    get comment() {
NEW
480
        return this.#yNode.comment ?? undefined;
×
481
    }
482
    set comment(comment: string | undefined) {
483
        // eslint-disable-next-line unicorn/no-null
NEW
484
        this.#yNode.comment = comment ?? null;
×
485
    }
486
    get commentBefore() {
NEW
487
        return this.#yNode.commentBefore ?? undefined;
×
488
    }
489
    set commentBefore(comment: string | undefined) {
490
        // eslint-disable-next-line unicorn/no-null
NEW
491
        this.#yNode.commentBefore = comment ?? null;
×
492
    }
493

494
    getValue<K extends keyof T>(key: K): T[K] | undefined {
NEW
495
        const node = getYamlNode(this.#yNode, key);
×
NEW
496
        if (!node) return undefined;
×
NEW
497
        return node.toJS(this.#doc) as T[K];
×
498
    }
499
    getNode<K extends keyof T>(key: K): RCfgNode<T[K]> | undefined {
NEW
500
        const node = getYamlNode(this.#yNode, key);
×
NEW
501
        if (!node) return undefined;
×
NEW
502
        return toConfigNode<T[K]>(this.#doc, node);
×
503
    }
504
    setValue<K extends KeyOf<T>>(key: K, value: NodeOrValue<ValueOf1<T, K>>): void {
NEW
505
        if (!isNodeValue(value)) {
×
NEW
506
            this.#yNode.set(key, value);
×
NEW
507
            return;
×
508
        }
NEW
509
        this.#yNode.set(key, value.value);
×
NEW
510
        const yNodeValue = getYamlNode(this.#yNode, key);
×
NEW
511
        assert(yNodeValue);
×
512
        // eslint-disable-next-line unicorn/no-null
NEW
513
        yNodeValue.comment = value.comment ?? null;
×
514
        // eslint-disable-next-line unicorn/no-null
NEW
515
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
516
    }
517
    delete<K extends KeyOf<T>>(key: K): boolean {
NEW
518
        return this.#yNode.delete(key);
×
519
    }
520
}
521

522
function toConfigObjectNode<T extends object>(doc: YamlDocument, yNode: YAMLMap): CfgObjectNode<T> {
523
    return new ConfigObjectNode<T>(doc, yNode as YAMLMap<KeyOf<T>, T[KeyOf<T>]>);
1✔
524
}
525

526
class ConfigScalarNode<T extends string | number | boolean | null | undefined>
527
    extends ConfigNodeBase<'scalar', T>
528
    implements CfgScalarNode<T>
529
{
530
    private $doc: YamlDocument;
531
    private $yNode: Scalar<T>;
532

533
    readonly type = 'scalar';
17✔
534

535
    constructor(doc: YamlDocument, yNode: Scalar<T>) {
536
        super('scalar');
17✔
537
        this.$doc = doc;
17✔
538
        this.$yNode = yNode;
17✔
539
        assert(isScalar(yNode), 'Expected yNode to be a Scalar');
17✔
540
    }
541

542
    get value() {
543
        return this.$yNode.toJS(this.$doc) as T;
5✔
544
    }
545

546
    set value(value: T) {
NEW
547
        this.$yNode.value = value;
×
548
    }
549

550
    get comment() {
551
        return this.$yNode.comment ?? undefined;
10✔
552
    }
553

554
    set comment(comment: string | undefined) {
555
        // eslint-disable-next-line unicorn/no-null
556
        this.$yNode.comment = comment ?? null;
2!
557
    }
558

559
    get commentBefore() {
560
        return this.$yNode.commentBefore ?? undefined;
10✔
561
    }
562

563
    set commentBefore(comment: string | undefined) {
564
        // eslint-disable-next-line unicorn/no-null
565
        this.$yNode.commentBefore = comment ?? null;
2!
566
    }
567

568
    toJSON() {
NEW
569
        return {
×
570
            type: this.type,
571
            value: this.value,
572
            comment: this.comment,
573
            commentBefore: this.commentBefore,
574
        };
575
    }
576
}
577

578
function toConfigScalarNode<T extends string | number | boolean | null | undefined>(
579
    doc: YamlDocument,
580
    yNode: Scalar,
581
): CfgScalarNode<T> {
582
    return new ConfigScalarNode<T>(doc, yNode as Scalar<T>);
17✔
583
}
584

585
function isYamlSeq<T>(node: YamlNode): node is YAMLSeq<T> {
586
    return isSeq(node);
28✔
587
}
588

589
function yamlNodeType(node: YamlNode): 'scalar' | 'seq' | 'map' | 'alias' | 'unknown' {
NEW
590
    if (isScalar(node)) return 'scalar';
×
NEW
591
    if (isSeq(node)) return 'seq';
×
NEW
592
    if (isMap(node)) return 'map';
×
NEW
593
    if (isAlias(node)) return 'alias';
×
NEW
594
    return 'unknown';
×
595
}
596

597
function setYamlNodeComments(yamlNode: YamlNode | undefined, comments: NodeComments): void {
598
    if (!yamlNode) return;
1!
599
    if ('comment' in comments) {
1!
600
        // eslint-disable-next-line unicorn/no-null
601
        yamlNode.comment = comments.comment ?? null;
1!
602
    }
603
    if ('commentBefore' in comments) {
1!
604
        // eslint-disable-next-line unicorn/no-null
605
        yamlNode.commentBefore = comments.commentBefore ?? null;
1!
606
    }
607
}
608

609
function setYamlNodeValue<T>(yamlNode: YamlNode, nodeValue: NodeValue<T>): void {
610
    setYamlNodeComments(yamlNode, nodeValue);
1✔
611
    if (isScalar(yamlNode)) {
1!
612
        yamlNode.value = nodeValue.value;
1✔
613
        return;
1✔
614
    }
NEW
615
    const value = nodeValue.value;
×
NEW
616
    if (isSeq(yamlNode)) {
×
NEW
617
        assert(Array.isArray(value), 'Expected value to be an array for YAMLSeq');
×
NEW
618
        yamlNode.items = [];
×
NEW
619
        for (let i = 0; i < value.length; ++i) {
×
NEW
620
            yamlNode.set(i, value[i]);
×
621
        }
NEW
622
        return;
×
623
    }
NEW
624
    if (isMap(yamlNode)) {
×
NEW
625
        assert(typeof value === 'object' && value !== null, 'Expected value to be an object for YAMLMap');
×
NEW
626
        yamlNode.items = [];
×
NEW
627
        for (const [key, val] of Object.entries(value)) {
×
NEW
628
            yamlNode.set(key, val);
×
629
        }
NEW
630
        return;
×
631
    }
NEW
632
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yamlNode)}`);
×
633
}
634

635
function findPair(yNode: YamlNode, yKey: string | Scalar<string>): Pair<Scalar<string> | string, YamlNode> | undefined {
636
    const key = isScalar(yKey) ? yKey.value : yKey;
15!
637
    if (!isMap(yNode)) return undefined;
15!
638
    const items = yNode.items as Pair<YamlNode | string, YamlNode>[];
15✔
639
    for (const item of items) {
15✔
640
        if (!isPair(item)) continue;
41!
641
        if (item.key === key) {
41✔
642
            return item as Pair<string, YamlNode>;
1✔
643
        }
644
        if (isScalar(item.key) && item.key.value === key) {
40✔
645
            return item as Pair<Scalar<string>, YamlNode>;
14✔
646
        }
647
    }
NEW
648
    return undefined;
×
649
}
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