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

streetsidesoftware / cspell / 15433608964

04 Jun 2025 04:30AM UTC coverage: 92.361% (-0.8%) from 93.187%
15433608964

Pull #7414

github

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

12867 of 15195 branches covered (84.68%)

221 of 393 new or added lines in 13 files covered. (56.23%)

15838 of 17148 relevant lines covered (92.36%)

30053.04 hits per line

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

76.04
/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
        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!
NEW
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 parseCSpellConfigFileYaml(file: TextFile): CSpellConfigFileYaml {
253
    const { url, content } = file;
29✔
254

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

274
function getScalarValue<T>(node: T | Scalar<T>): T {
275
    if (isScalar(node)) {
259✔
276
        return node.value;
196✔
277
    }
278
    return node;
63✔
279
}
280

281
function toScalar<T>(node: T | Scalar<T>): Scalar<T> {
282
    if (isScalar(node)) {
78✔
283
        return node;
60✔
284
    }
285
    return new Scalar(node);
18✔
286
}
287

288
type StringOrScalar = string | Scalar<string>;
289

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

307
function isSectionHeader(word: StringOrScalar): boolean {
308
    if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false;
67✔
309
    if (word.spaceBefore) return true;
16✔
310
    if (!word.commentBefore) return false;
8!
311
    return word.commentBefore.includes('\n\n');
8✔
312
}
313

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

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

347
function sortWords(words: StringOrScalar[]): StringOrScalar[] {
348
    const compare = new Intl.Collator().compare;
7✔
349

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

364
    const result = groups.flat();
7✔
365
    return result.map((w) => toScalar(w));
67✔
366
}
367

368
function cloneWord(word: StringOrScalar): StringOrScalar {
369
    if (isScalar(word)) {
67✔
370
        return word.clone() as Scalar<string>;
49✔
371
    }
372
    return word;
18✔
373
}
374

375
function getYamlNode(yamlDoc: YamlDocument | YAMLMap | YAMLSeq, key: unknown | unknown[]): YamlNode | undefined {
376
    return (Array.isArray(key) ? yamlDoc.getIn(key, true) : yamlDoc.get(key, true)) as YamlNode | undefined;
30!
377
}
378

379
type ArrayType<T> = T extends unknown[] ? T[number] : never;
380

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

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

397
    abstract value: T;
398
    abstract comment: string | undefined;
399
    abstract commentBefore: string | undefined;
400
}
401

402
class ConfigArrayNode<T extends unknown[]>
403
    extends ConfigNodeBase<'array', ArrayType<T>[]>
404
    implements CfgArrayNode<ArrayType<T>>
405
{
406
    #doc: YamlDocument;
407
    #yNode: YAMLSeq<ArrayType<T>>;
408

409
    constructor(doc: YamlDocument, yNode: YAMLSeq<ArrayType<T>>) {
410
        super('array');
10✔
411
        this.#doc = doc;
10✔
412
        this.#yNode = yNode;
10✔
413
    }
414

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

433
    getNode(key: number) {
434
        const node = getYamlNode(this.#yNode, key);
4✔
435
        if (!node) return undefined;
4!
436
        return toConfigNode<ArrayType<T>>(this.#doc, node) as RCfgNode<ArrayType<T>>;
4✔
437
    }
438

439
    getValue(key: number): ArrayType<T> | undefined {
440
        const node = getYamlNode(this.#yNode, key);
1✔
441
        if (!node) return undefined;
1!
442
        return node.toJS(this.#doc) as ArrayType<T>;
1✔
443
    }
444

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

459
    delete(key: number): boolean {
NEW
460
        return this.#yNode.delete(key);
×
461
    }
462

463
    push(value: NodeOrValue<ArrayType<T>>): number {
NEW
464
        if (!isNodeValue(value)) {
×
NEW
465
            this.#yNode.add(value);
×
NEW
466
            return this.#yNode.items.length;
×
467
        }
NEW
468
        this.#yNode.add(value.value);
×
469

NEW
470
        setYamlNodeComments(getYamlNode(this.#yNode, this.#yNode.items.length - 1), value);
×
NEW
471
        return this.#yNode.items.length;
×
472
    }
473

474
    get length(): number {
NEW
475
        return this.#yNode.items.length;
×
476
    }
477
}
478

479
function toConfigArrayNode<T extends unknown[]>(doc: YamlDocument, yNode: YAMLSeq): CfgArrayNode<ArrayType<T>> {
480
    return new ConfigArrayNode<T>(doc, yNode as YAMLSeq<ArrayType<T>>);
10✔
481
}
482

483
class ConfigObjectNode<T extends object> extends ConfigNodeBase<'object', T> implements CfgObjectNode<T> {
484
    #doc: YamlDocument;
485
    #yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>;
486

487
    constructor(doc: YamlDocument, yNode: YAMLMap<KeyOf<T>, T[KeyOf<T>]>) {
488
        super('object');
1✔
489
        this.#doc = doc;
1✔
490
        this.#yNode = yNode;
1✔
491
    }
492

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

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

539
function toConfigObjectNode<T extends object>(doc: YamlDocument, yNode: YAMLMap): CfgObjectNode<T> {
540
    return new ConfigObjectNode<T>(doc, yNode as YAMLMap<KeyOf<T>, T[KeyOf<T>]>);
1✔
541
}
542

543
class ConfigScalarNode<T extends string | number | boolean | null | undefined>
544
    extends ConfigNodeBase<'scalar', T>
545
    implements CfgScalarNode<T>
546
{
547
    private $doc: YamlDocument;
548
    private $yNode: Scalar<T>;
549

550
    readonly type = 'scalar';
17✔
551

552
    constructor(doc: YamlDocument, yNode: Scalar<T>) {
553
        super('scalar');
17✔
554
        this.$doc = doc;
17✔
555
        this.$yNode = yNode;
17✔
556
        assert(isScalar(yNode), 'Expected yNode to be a Scalar');
17✔
557
    }
558

559
    get value() {
560
        return this.$yNode.toJS(this.$doc) as T;
5✔
561
    }
562

563
    set value(value: T) {
NEW
564
        this.$yNode.value = value;
×
565
    }
566

567
    get comment() {
568
        return this.$yNode.comment ?? undefined;
10✔
569
    }
570

571
    set comment(comment: string | undefined) {
572
        // eslint-disable-next-line unicorn/no-null
573
        this.$yNode.comment = comment ?? null;
2!
574
    }
575

576
    get commentBefore() {
577
        return this.$yNode.commentBefore ?? undefined;
10✔
578
    }
579

580
    set commentBefore(comment: string | undefined) {
581
        // eslint-disable-next-line unicorn/no-null
582
        this.$yNode.commentBefore = comment ?? null;
2!
583
    }
584

585
    toJSON() {
NEW
586
        return {
×
587
            type: this.type,
588
            value: this.value,
589
            comment: this.comment,
590
            commentBefore: this.commentBefore,
591
        };
592
    }
593
}
594

595
function toConfigScalarNode<T extends string | number | boolean | null | undefined>(
596
    doc: YamlDocument,
597
    yNode: Scalar,
598
): CfgScalarNode<T> {
599
    return new ConfigScalarNode<T>(doc, yNode as Scalar<T>);
17✔
600
}
601

602
function isYamlSeq<T>(node: YamlNode): node is YAMLSeq<T> {
603
    return isSeq(node);
28✔
604
}
605

606
function yamlNodeType(node: YamlNode): 'scalar' | 'seq' | 'map' | 'alias' | 'unknown' {
NEW
607
    if (isScalar(node)) return 'scalar';
×
NEW
608
    if (isSeq(node)) return 'seq';
×
NEW
609
    if (isMap(node)) return 'map';
×
NEW
610
    if (isAlias(node)) return 'alias';
×
NEW
611
    return 'unknown';
×
612
}
613

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

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

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

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