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

streetsidesoftware / cspell / 15425583305

03 Jun 2025 06:59PM UTC coverage: 92.385% (-0.8%) from 93.187%
15425583305

Pull #7414

github

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

12827 of 15127 branches covered (84.8%)

182 of 345 new or added lines in 13 files covered. (52.75%)

4 existing lines in 2 files now uncovered.

15796 of 17098 relevant lines covered (92.39%)

30173.04 hits per line

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

73.66
/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
    isPair,
9
    isScalar,
10
    isSeq,
11
    type Node as YamlNode,
12
    type Pair,
13
    parseDocument,
14
    Scalar,
15
    stringify,
16
    visit as yamlWalkAst,
17
    YAMLMap,
18
    YAMLSeq,
19
} from 'yaml';
20

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

37
type S = CSpellSettings;
38

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

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

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

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

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

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

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

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

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

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

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

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

151
    get comment(): string | undefined {
NEW
152
        return this.yamlDoc.comment ?? undefined;
×
153
    }
154

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

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

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

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

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

197
        return this;
4✔
198
    }
199

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

208
    static parse(file: TextFile): CSpellConfigFileYaml {
209
        return parseCSpellConfigFileYaml(file);
1✔
210
    }
211
}
212

213
export function parseCSpellConfigFileYaml(file: TextFile): CSpellConfigFileYaml {
214
    const { url, content } = file;
29✔
215

216
    try {
29✔
217
        const doc = parseDocument<YAMLMap | Scalar<null | string>>(content);
29✔
218
        // Force empty content to be a map.
219
        if (doc.contents === null || (isScalar(doc.contents) && !doc.contents.value)) {
29✔
220
            doc.contents = new YAMLMap();
3✔
221
        }
222
        if (!isMap(doc.contents)) {
29✔
223
            throw new ParseError(url, `Invalid YAML content ${url}`);
3✔
224
        }
225
        const indent = detectIndentAsNum(content);
26✔
226
        return new CSpellConfigFileYaml(url, doc, indent);
26✔
227
    } catch (e) {
228
        if (e instanceof ParseError) {
3!
229
            throw e;
3✔
230
        }
NEW
231
        throw new ParseError(url, undefined, { cause: e });
×
232
    }
233
}
234

235
function getScalarValue<T>(node: T | Scalar<T>): T {
236
    if (isScalar(node)) {
259✔
237
        return node.value;
196✔
238
    }
239
    return node;
63✔
240
}
241

242
function toScalar<T>(node: T | Scalar<T>): Scalar<T> {
243
    if (isScalar(node)) {
78✔
244
        return node;
60✔
245
    }
246
    return new Scalar(node);
18✔
247
}
248

249
type StringOrScalar = string | Scalar<string>;
250

251
function groupWords(words: StringOrScalar[]): StringOrScalar[][] {
252
    const groups: StringOrScalar[][] = [];
7✔
253
    if (words.length === 0) {
7!
UNCOV
254
        return groups;
×
255
    }
256
    let currentGroup: StringOrScalar[] = [];
7✔
257
    groups.push(currentGroup);
7✔
258
    for (const word of words) {
7✔
259
        if (isSectionHeader(word)) {
67✔
260
            currentGroup = [];
8✔
261
            groups.push(currentGroup);
8✔
262
        }
263
        currentGroup.push(cloneWord(word));
67✔
264
    }
265
    return groups;
7✔
266
}
267

268
function isSectionHeader(word: StringOrScalar): boolean {
269
    if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false;
67✔
270
    if (word.spaceBefore) return true;
16✔
271
    if (!word.commentBefore) return false;
8!
272
    return word.commentBefore.includes('\n\n');
8✔
273
}
274

275
function adjustSectionHeader(word: Scalar<string>, prev: StringOrScalar, isFirstSection: boolean): void {
276
    // console.log('adjustSectionHeader %o', { word, prev, isFirstSection });
277
    if (!isScalar(prev)) return;
11!
278
    let captureComment = isFirstSection;
11✔
279
    if (prev.spaceBefore) {
11✔
280
        word.spaceBefore = true;
6✔
281
        captureComment = true;
6✔
282
        delete prev.spaceBefore;
6✔
283
    }
284
    if (!prev.commentBefore) return;
11✔
285

286
    const originalComment = prev.commentBefore;
5✔
287
    const lines = originalComment.split(/^\n/gm);
5✔
288
    const lastLine = lines[lines.length - 1];
5✔
289
    // console.log('adjustSectionHeader lines %o', { lines, isFirstSection, lastLine, originalComment });
290
    captureComment = (captureComment && originalComment.trim() === lastLine.trim()) || originalComment.endsWith('\n');
5!
291
    let header = originalComment;
5✔
292
    if (captureComment) {
5✔
293
        delete prev.commentBefore;
3✔
294
    } else {
295
        prev.commentBefore = lastLine;
2✔
296
        lines.pop();
2✔
297
        header = lines.join('\n');
2✔
298
    }
299
    if (word.commentBefore) {
5✔
300
        header += header.endsWith('\n\n') ? '' : '\n';
2!
301
        header += header.endsWith('\n\n') ? '' : '\n';
2✔
302
        header += word.commentBefore;
2✔
303
    }
304
    word.commentBefore = header;
5✔
305
    // console.log('adjustSectionHeader after %o', { word, prev, isFirstSection, originalComment, lastLine, lines });
306
}
307

308
function sortWords(words: StringOrScalar[]): StringOrScalar[] {
309
    const compare = new Intl.Collator().compare;
7✔
310

311
    const groups = groupWords(words);
7✔
312
    let firstGroup = true;
7✔
313
    for (const group of groups) {
7✔
314
        const head = group[0];
15✔
315
        group.sort((a, b) => {
15✔
316
            return compare(getScalarValue(a), getScalarValue(b));
105✔
317
        });
318
        if (group[0] !== head && isScalar(head)) {
15✔
319
            const first = (group[0] = toScalar(group[0]));
11✔
320
            adjustSectionHeader(first, head, firstGroup);
11✔
321
        }
322
        firstGroup = false;
15✔
323
    }
324

325
    const result = groups.flat();
7✔
326
    return result.map((w) => toScalar(w));
67✔
327
}
328

329
function cloneWord(word: StringOrScalar): StringOrScalar {
330
    if (isScalar(word)) {
67✔
331
        return word.clone() as Scalar<string>;
49✔
332
    }
333
    return word;
18✔
334
}
335

336
function getYamlNode(yamlDoc: YamlDocument | YAMLMap | YAMLSeq, key: unknown | unknown[]): YamlNode | undefined {
337
    return (Array.isArray(key) ? yamlDoc.getIn(key, true) : yamlDoc.get(key, true)) as YamlNode | undefined;
30!
338
}
339

340
type ArrayType<T> = T extends unknown[] ? T[number] : never;
341

342
function toConfigNode<T>(doc: YamlDocument, yNode: YamlNode): RCfgNode<T> {
343
    if (isYamlSeq(yNode)) {
28✔
344
        return toConfigArrayNode(doc, yNode) as RCfgNode<T>;
10✔
345
    }
346
    if (isMap(yNode)) {
18✔
347
        return toConfigObjectNode(doc, yNode) as RCfgNode<T>;
1✔
348
    }
349
    if (isScalar(yNode)) {
17!
350
        return toConfigScalarNode(doc, yNode) as RCfgNode<T>;
17✔
351
    }
UNCOV
352
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yNode)}`);
×
353
}
354

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

358
    abstract value: T;
359
    abstract comment: string | undefined;
360
    abstract commentBefore: string | undefined;
361
}
362

363
class ConfigArrayNode<T extends unknown[]>
364
    extends ConfigNodeBase<'array', ArrayType<T>[]>
365
    implements CfgArrayNode<ArrayType<T>>
366
{
367
    #doc: YamlDocument;
368
    #yNode: YAMLSeq<ArrayType<T>>;
369

370
    constructor(doc: YamlDocument, yNode: YAMLSeq<ArrayType<T>>) {
371
        super('array');
10✔
372
        this.#doc = doc;
10✔
373
        this.#yNode = yNode;
10✔
374
    }
375

376
    get value(): ArrayType<T>[] {
NEW
377
        return this.#yNode.toJS(this.#doc) as ArrayType<T>[];
×
378
    }
379
    get comment() {
380
        return this.#yNode.comment ?? undefined;
1✔
381
    }
382
    set comment(comment: string | undefined) {
383
        // eslint-disable-next-line unicorn/no-null
NEW
384
        this.#yNode.comment = comment ?? null;
×
385
    }
386
    get commentBefore() {
387
        return this.#yNode.commentBefore ?? undefined;
1!
388
    }
389
    set commentBefore(comment: string | undefined) {
390
        // eslint-disable-next-line unicorn/no-null
NEW
391
        this.#yNode.commentBefore = comment ?? null;
×
392
    }
393

394
    getNode(key: number) {
395
        const node = getYamlNode(this.#yNode, key);
4✔
396
        if (!node) return undefined;
4!
397
        return toConfigNode<ArrayType<T>>(this.#doc, node) as RCfgNode<ArrayType<T>>;
4✔
398
    }
399

400
    getValue(key: number): ArrayType<T> | undefined {
401
        const node = getYamlNode(this.#yNode, key);
1✔
402
        if (!node) return undefined;
1!
403
        return node.toJS(this.#doc) as ArrayType<T>;
1✔
404
    }
405

406
    setValue(key: number, value: NodeOrValue<ArrayType<T>>): void {
407
        if (!isNodeValue(value)) {
1!
408
            this.#yNode.set(key, value);
1✔
409
            return;
1✔
410
        }
NEW
411
        this.#yNode.set(key, value.value);
×
NEW
412
        const yNodeValue = getYamlNode(this.#yNode, key);
×
NEW
413
        assert(yNodeValue);
×
414
        // eslint-disable-next-line unicorn/no-null
NEW
415
        yNodeValue.comment = value.comment ?? null;
×
416
        // eslint-disable-next-line unicorn/no-null
NEW
417
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
418
    }
419

420
    delete(key: number): boolean {
NEW
421
        return this.#yNode.delete(key);
×
422
    }
423

424
    push(value: NodeOrValue<ArrayType<T>>): number {
NEW
425
        if (!isNodeValue(value)) {
×
NEW
426
            this.#yNode.add(value);
×
NEW
427
            return this.#yNode.items.length;
×
428
        }
NEW
429
        this.#yNode.add(value.value);
×
430

NEW
431
        setYamlNodeComments(getYamlNode(this.#yNode, this.#yNode.items.length - 1), value);
×
NEW
432
        return this.#yNode.items.length;
×
433
    }
434

435
    get length(): number {
NEW
436
        return this.#yNode.items.length;
×
437
    }
438
}
439

440
function toConfigArrayNode<T extends unknown[]>(doc: YamlDocument, yNode: YAMLSeq): CfgArrayNode<ArrayType<T>> {
441
    return new ConfigArrayNode<T>(doc, yNode as YAMLSeq<ArrayType<T>>);
10✔
442
}
443

444
class ConfigObjectNode<T extends object> extends ConfigNodeBase<'object', T> implements CfgObjectNode<T> {
445
    #doc: YamlDocument;
446
    #yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>;
447

448
    constructor(doc: YamlDocument, yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>) {
449
        super('object');
1✔
450
        this.#doc = doc;
1✔
451
        this.#yNode = yNode;
1✔
452
    }
453

454
    get value(): T {
NEW
455
        return this.#yNode.toJS(this.#doc) as T;
×
456
    }
457
    get comment() {
NEW
458
        return this.#yNode.comment ?? undefined;
×
459
    }
460
    set comment(comment: string | undefined) {
461
        // eslint-disable-next-line unicorn/no-null
NEW
462
        this.#yNode.comment = comment ?? null;
×
463
    }
464
    get commentBefore() {
NEW
465
        return this.#yNode.commentBefore ?? undefined;
×
466
    }
467
    set commentBefore(comment: string | undefined) {
468
        // eslint-disable-next-line unicorn/no-null
NEW
469
        this.#yNode.commentBefore = comment ?? null;
×
470
    }
471

472
    getValue<K extends keyof T>(key: K): T[K] | undefined {
NEW
473
        const node = getYamlNode(this.#yNode, key);
×
NEW
474
        if (!node) return undefined;
×
NEW
475
        return node.toJS(this.#doc) as T[K];
×
476
    }
477
    getNode<K extends keyof T>(key: K): RCfgNode<T[K]> | undefined {
NEW
478
        const node = getYamlNode(this.#yNode, key);
×
NEW
479
        if (!node) return undefined;
×
NEW
480
        return toConfigNode<T[K]>(this.#doc, node);
×
481
    }
482
    setValue<K extends KeyOf<T>>(key: K, value: NodeOrValue<ValueOf1<T, K>>): void {
NEW
483
        if (!isNodeValue(value)) {
×
NEW
484
            this.#yNode.set(key, value);
×
NEW
485
            return;
×
486
        }
NEW
487
        this.#yNode.set(key, value.value);
×
NEW
488
        const yNodeValue = getYamlNode(this.#yNode, key);
×
NEW
489
        assert(yNodeValue);
×
490
        // eslint-disable-next-line unicorn/no-null
NEW
491
        yNodeValue.comment = value.comment ?? null;
×
492
        // eslint-disable-next-line unicorn/no-null
NEW
493
        yNodeValue.commentBefore = value.commentBefore ?? null;
×
494
    }
495
    delete<K extends KeyOf<T>>(key: K): boolean {
NEW
496
        return this.#yNode.delete(key);
×
497
    }
498
}
499

500
function toConfigObjectNode<T extends object>(doc: YamlDocument, yNode: YAMLMap): CfgObjectNode<T> {
501
    return new ConfigObjectNode<T>(doc, yNode as YAMLMap<KeyOf<T>, T[KeyOf<T>]>);
1✔
502
}
503

504
class ConfigScalarNode<T extends string | number | boolean | null | undefined>
505
    extends ConfigNodeBase<'scalar', T>
506
    implements CfgScalarNode<T>
507
{
508
    private $doc: YamlDocument;
509
    private $yNode: Scalar<T>;
510

511
    readonly type = 'scalar';
17✔
512

513
    constructor(doc: YamlDocument, yNode: Scalar<T>) {
514
        super('scalar');
17✔
515
        this.$doc = doc;
17✔
516
        this.$yNode = yNode;
17✔
517
        assert(isScalar(yNode), 'Expected yNode to be a Scalar');
17✔
518
    }
519

520
    get value() {
521
        return this.$yNode.toJS(this.$doc) as T;
5✔
522
    }
523

524
    set value(value: T) {
NEW
525
        this.$yNode.value = value;
×
526
    }
527

528
    get comment() {
529
        return this.$yNode.comment ?? undefined;
10✔
530
    }
531

532
    set comment(comment: string | undefined) {
533
        // eslint-disable-next-line unicorn/no-null
534
        this.$yNode.comment = comment ?? null;
2!
535
    }
536

537
    get commentBefore() {
538
        return this.$yNode.commentBefore ?? undefined;
10✔
539
    }
540

541
    set commentBefore(comment: string | undefined) {
542
        // eslint-disable-next-line unicorn/no-null
543
        this.$yNode.commentBefore = comment ?? null;
2!
544
    }
545

546
    toJSON() {
NEW
547
        return {
×
548
            type: this.type,
549
            value: this.value,
550
            comment: this.comment,
551
            commentBefore: this.commentBefore,
552
        };
553
    }
554
}
555

556
function toConfigScalarNode<T extends string | number | boolean | null | undefined>(
557
    doc: YamlDocument,
558
    yNode: Scalar,
559
): CfgScalarNode<T> {
560
    return new ConfigScalarNode<T>(doc, yNode as Scalar<T>);
17✔
561
}
562

563
function isYamlSeq<T>(node: YamlNode): node is YAMLSeq<T> {
564
    return isSeq(node);
28✔
565
}
566

567
function yamlNodeType(node: YamlNode): 'scalar' | 'seq' | 'map' | 'alias' | 'unknown' {
NEW
568
    if (isScalar(node)) return 'scalar';
×
NEW
569
    if (isSeq(node)) return 'seq';
×
NEW
570
    if (isMap(node)) return 'map';
×
NEW
571
    if (isAlias(node)) return 'alias';
×
NEW
572
    return 'unknown';
×
573
}
574

575
function setYamlNodeComments(yamlNode: YamlNode | undefined, comments: NodeComments): void {
576
    if (!yamlNode) return;
1!
577
    if ('comment' in comments) {
1!
578
        // eslint-disable-next-line unicorn/no-null
579
        yamlNode.comment = comments.comment ?? null;
1!
580
    }
581
    if ('commentBefore' in comments) {
1!
582
        // eslint-disable-next-line unicorn/no-null
583
        yamlNode.commentBefore = comments.commentBefore ?? null;
1!
584
    }
585
}
586

587
function setYamlNodeValue<T>(yamlNode: YamlNode, nodeValue: NodeValue<T>): void {
588
    setYamlNodeComments(yamlNode, nodeValue);
1✔
589
    if (isScalar(yamlNode)) {
1!
590
        yamlNode.value = nodeValue.value;
1✔
591
        return;
1✔
592
    }
NEW
593
    const value = nodeValue.value;
×
NEW
594
    if (isSeq(yamlNode)) {
×
NEW
595
        assert(Array.isArray(value), 'Expected value to be an array for YAMLSeq');
×
NEW
596
        yamlNode.items = [];
×
NEW
597
        for (let i = 0; i < value.length; ++i) {
×
NEW
598
            yamlNode.set(i, value[i]);
×
599
        }
NEW
600
        return;
×
601
    }
NEW
602
    if (isMap(yamlNode)) {
×
NEW
603
        assert(typeof value === 'object' && value !== null, 'Expected value to be an object for YAMLMap');
×
NEW
604
        yamlNode.items = [];
×
NEW
605
        for (const [key, val] of Object.entries(value)) {
×
NEW
606
            yamlNode.set(key, val);
×
607
        }
NEW
608
        return;
×
609
    }
NEW
610
    throw new Error(`Unsupported YAML node type: ${yamlNodeType(yamlNode)}`);
×
611
}
612

613
function findPair(yNode: YamlNode, key: string): Pair<Scalar<string>, YamlNode> | undefined {
614
    if (!isMap(yNode)) return undefined;
6!
615
    const items = yNode.items as Pair<YamlNode, YamlNode>[];
6✔
616
    for (const item of items) {
6✔
617
        if (!isPair(item)) continue;
23!
618
        if (isScalar(item.key) && item.key.value === key) {
23✔
619
            return item as Pair<Scalar<string>, YamlNode>;
6✔
620
        }
621
    }
NEW
622
    return undefined;
×
623
}
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