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

dart-lang / yaml_edit / 4394861413

pending completion
4394861413

push

github

GitHub
Require Dart 2.19, update to latest lints

28 of 30 new or added lines in 6 files covered. (93.33%)

624 of 654 relevant lines covered (95.41%)

56.99 hits per line

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

94.59
/lib/src/editor.dart
1
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
2
// for details. All rights reserved. Use of this source code is governed by a
3
// BSD-style license that can be found in the LICENSE file.
4

5
import 'package:meta/meta.dart';
6
import 'package:yaml/yaml.dart';
7

8
import 'equality.dart';
9
import 'errors.dart';
10
import 'list_mutations.dart';
11
import 'map_mutations.dart';
12
import 'source_edit.dart';
13
import 'strings.dart';
14
import 'utils.dart';
15
import 'wrap.dart';
16

17
/// An interface for modifying [YAML][1] documents while preserving comments
18
/// and whitespaces.
19
///
20
/// YAML parsing is supported by `package:yaml`, and modifications are performed
21
/// as string operations. An error will be thrown if internal assertions fail -
22
/// such a situation should be extremely rare, and should only occur with
23
/// degenerate formatting.
24
///
25
/// Most modification methods require the user to pass in an `Iterable<Object>`
26
/// path that holds the keys/indices to navigate to the element.
27
///
28
/// **Example:**
29
/// ```yaml
30
/// a: 1
31
/// b: 2
32
/// c:
33
///   - 3
34
///   - 4
35
///   - {e: 5, f: [6, 7]}
36
/// ```
37
///
38
/// To get to `7`, our path will be `['c', 2, 'f', 1]`. The path for the base
39
/// object is the empty array `[]`. All modification methods will throw a
40
/// [ArgumentError] if the path provided is invalid. Note also that that the
41
/// order of elements in the path is important, and it should be arranged in
42
/// order of calling, with the first element being the first key or index to be
43
/// called.
44
///
45
/// In most modification methods, users are required to pass in a value to be
46
/// used for updating the YAML tree. This value is only allowed to either be a
47
/// valid scalar that is recognizable by YAML (i.e. `bool`, `String`, `List`,
48
/// `Map`, `num`, `null`) or a [YamlNode]. Should the user want to specify
49
/// the style to be applied to the value passed in, the user may wrap the value
50
/// using [wrapAsYamlNode] while passing in the appropriate `scalarStyle` or
51
/// `collectionStyle`. While we try to respect the style that is passed in,
52
/// there will be instances where the formatting will not result in valid YAML,
53
/// and as such we will fallback to a default formatting while preserving the
54
/// content.
55
///
56
/// To dump the YAML after all the modifications have been completed, simply
57
/// call [toString()].
58
///
59
/// [1]: https://yaml.org/
60
@sealed
61
class YamlEditor {
62
  final List<SourceEdit> _edits = [];
48✔
63

64
  /// List of [SourceEdit]s that have been applied to [_yaml] since the creation
65
  /// of this instance, in chronological order. Intended to be compatible with
66
  /// `package:analysis_server`.
67
  ///
68
  /// The [SourceEdit] objects can be serialized to JSON using the `toJSON`
69
  /// function, deserialized using [SourceEdit.fromJson], and applied to a
70
  /// string using the `apply` function. Multiple [SourceEdit]s can be applied
71
  /// to a string using [SourceEdit.applyAll].
72
  ///
73
  /// For more information, refer to the [SourceEdit] class.
74
  List<SourceEdit> get edits => [..._edits];
12✔
75

76
  /// Current YAML string.
77
  String _yaml;
78

79
  /// Root node of YAML AST.
80
  YamlNode _contents;
81

82
  /// Stores the list of nodes in [_contents] that are connected by aliases.
83
  ///
84
  /// When a node is anchored with an alias and subsequently referenced,
85
  /// the full content of the anchored node is thought to be copied in the
86
  /// following references.
87
  ///
88
  /// **Example:**
89
  /// ```dart
90
  /// a: &SS Sammy Sosa
91
  /// b: *SS
92
  /// ```
93
  ///
94
  /// is equivalent to
95
  ///
96
  /// ```dart
97
  /// a: Sammy Sosa
98
  /// b: Sammy Sosa
99
  /// ```
100
  ///
101
  /// As such, aliased nodes have to be treated with special caution when
102
  /// any modification is taking place.
103
  ///
104
  /// See 7.1 Alias Nodes: https://yaml.org/spec/1.2/spec.html#id2786196
105
  Set<YamlNode> _aliases = {};
48✔
106

107
  /// Returns the current YAML string.
108
  @override
45✔
109
  String toString() => _yaml;
87✔
110

111
  factory YamlEditor(String yaml) => YamlEditor._(yaml);
138✔
112

113
  YamlEditor._(this._yaml) : _contents = loadYamlNode(_yaml) {
150✔
114
    _initialize();
99✔
115
  }
36✔
116

117
  /// Traverses the YAML tree formed to detect alias nodes.
118
  void _initialize() {
99✔
119
    _aliases = {};
99✔
120

121
    /// Performs a DFS on [_contents] to detect alias nodes.
122
    final visited = <YamlNode>{};
48✔
123
    void collectAliases(YamlNode node) {
99✔
124
      if (visited.add(node)) {
99✔
125
        if (node is YamlMap) {
96✔
126
          node.nodes.forEach((key, value) {
165✔
127
            collectAliases(key as YamlNode);
81✔
128
            collectAliases(value);
81✔
129
          });
39✔
130
        } else if (node is YamlList) {
96✔
131
          node.nodes.forEach(collectAliases);
123✔
132
        }
133
      } else {
134
        _aliases.add(node);
9✔
135
      }
136
    }
48✔
137

138
    collectAliases(_contents);
150✔
139
  }
48✔
140

141
  /// Parses the document to return [YamlNode] currently present at [path].
142
  ///
143
  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
144
  /// function is returned.
145
  ///
146
  /// If [orElse] is omitted, it defaults to throwing a [ArgumentError].
147
  ///
148
  /// To get a default value when [path] does not point to a value in the
149
  /// [YamlNode]-tree, simply pass `orElse: () => ...`.
150
  ///
151
  /// **Example:** (using orElse)
152
  /// ```dart
153
  /// final myYamlEditor('{"key": "value"}');
154
  /// final node = myYamlEditor.valueAt(
155
  ///   ['invalid', 'path'],
156
  ///   orElse: () => wrapAsYamlNode(null),
157
  /// );
158
  /// print(node.value); // null
159
  /// ```
160
  ///
161
  /// **Example:** (common usage)
162
  /// ```dart
163
  ///   final doc = YamlEditor('''
164
  /// a: 1
165
  /// b:
166
  ///   d: 4
167
  ///   e: [5, 6, 7]
168
  /// c: 3
169
  /// ''');
170
  /// print(doc.parseAt(['b', 'e', 2])); // 7
171
  /// ```
172
  /// The value returned by [parseAt] is invalidated when the documented is
173
  /// mutated, as illustrated below:
174
  ///
175
  /// **Example:** (old [parseAt] value is invalidated)
176
  /// ```dart
177
  /// final doc = YamlEditor("YAML: YAML Ain't Markup Language");
178
  /// final node = doc.parseAt(['YAML']);
179
  ///
180
  /// print(node.value); // Expected output: "YAML Ain't Markup Language"
181
  ///
182
  /// doc.update(['YAML'], 'YAML');
183
  ///
184
  /// final newNode = doc.parseAt(['YAML']);
185
  ///
186
  /// // Note that the value does not change
187
  /// print(newNode.value); // "YAML"
188
  /// print(node.value); // "YAML Ain't Markup Language"
189
  /// ```
190
  YamlNode parseAt(Iterable<Object?> path, {YamlNode Function()? orElse}) {
42✔
191
    return _traverse(path, orElse: orElse);
81✔
192
  }
193

194
  /// Sets [value] in the [path].
195
  ///
196
  /// There is a subtle difference between [update] and [remove] followed by
197
  /// an [insertIntoList], because [update] preserves comments at the same
198
  /// level.
199
  ///
200
  /// Throws a [ArgumentError] if [path] is invalid.
201
  ///
202
  /// **Example:** (using [update])
203
  /// ```dart
204
  /// final doc = YamlEditor('''
205
  ///   - 0
206
  ///   - 1 # comment
207
  ///   - 2
208
  /// ''');
209
  /// doc.update([1], 'test');
210
  /// ```
211
  ///
212
  /// **Expected Output:**
213
  /// ```yaml
214
  ///   - 0
215
  ///   - test # comment
216
  ///   - 2
217
  /// ```
218
  ///
219
  /// **Example:** (using [remove] and [insertIntoList])
220
  /// ```dart
221
  /// final doc2 = YamlEditor('''
222
  ///   - 0
223
  ///   - 1 # comment
224
  ///   - 2
225
  /// ''');
226
  /// doc2.remove([1]);
227
  /// doc2.insertIntoList([], 1, 'test');
228
  /// ```
229
  ///
230
  /// **Expected Output:**
231
  /// ```yaml
232
  ///   - 0
233
  ///   - test
234
  ///   - 2
235
  /// ```
236
  void update(Iterable<Object?> path, Object? value) {
51✔
237
    final valueNode = wrapAsYamlNode(value);
51✔
238

239
    if (path.isEmpty) {
27✔
240
      final start = _contents.span.start.offset;
75✔
241
      final end = getContentSensitiveEnd(_contents);
45✔
242
      final lineEnding = getLineEnding(_yaml);
45✔
243
      final edit = SourceEdit(
15✔
244
          start, end - start, yamlEncodeBlockString(valueNode, 0, lineEnding));
45✔
245

246
      return _performEdit(edit, path, valueNode);
30✔
247
    }
248

249
    final pathAsList = path.toList();
21✔
250
    final collectionPath = pathAsList.take(path.length - 1);
81✔
251
    final keyOrIndex = pathAsList.last;
39✔
252
    final parentNode = _traverse(collectionPath, checkAlias: true);
39✔
253

254
    if (parentNode is YamlList) {
39✔
255
      if (keyOrIndex is! int) {
18✔
256
        throw PathError(path, path, parentNode);
6✔
257
      }
258
      final expected = wrapAsYamlNode(
33✔
259
        [...parentNode.nodes]..[keyOrIndex] = valueNode,
69✔
260
      );
261

262
      return _performEdit(updateInList(this, parentNode, keyOrIndex, valueNode),
51✔
263
          collectionPath, expected);
264
    }
265

266
    if (parentNode is YamlMap) {
39✔
267
      final expectedMap =
268
          updatedYamlMap(parentNode, (nodes) => nodes[keyOrIndex] = valueNode);
81✔
269
      return _performEdit(updateInMap(this, parentNode, keyOrIndex, valueNode),
60✔
270
          collectionPath, expectedMap);
271
    }
272

273
    throw PathError.unexpected(
6✔
274
        path, 'Scalar $parentNode does not have key $keyOrIndex');
6✔
275
  }
24✔
276

277
  /// Appends [value] to the list at [path].
278
  ///
279
  /// Throws a [ArgumentError] if the element at the given path is not a
280
  /// [YamlList] or if the path is invalid.
281
  ///
282
  /// **Example:**
283
  /// ```dart
284
  /// final doc = YamlEditor('[0, 1]');
285
  /// doc.appendToList([], 2); // [0, 1, 2]
286
  /// ```
287
  void appendToList(Iterable<Object?> path, Object? value) {
18✔
288
    final yamlList = _traverseToList(path);
27✔
289

290
    insertIntoList(path, yamlList.length, value);
42✔
291
  }
3✔
292

293
  /// Prepends [value] to the list at [path].
294
  ///
295
  /// Throws a [ArgumentError] if the element at the given path is not a
296
  /// [YamlList] or if the path is invalid.
297
  ///
298
  /// **Example:**
299
  /// ```dart
300
  /// final doc = YamlEditor('[1, 2]');
301
  /// doc.prependToList([], 0); // [0, 1, 2]
302
  /// ```
303
  void prependToList(Iterable<Object?> path, Object? value) {
15✔
304
    insertIntoList(path, 0, value);
24✔
305
  }
306

307
  /// Inserts [value] into the list at [path].
308
  ///
309
  /// [index] must be non-negative and no greater than the list's length.
310
  ///
311
  /// Throws a [ArgumentError] if the element at the given path is not a
312
  /// [YamlList] or if the path is invalid.
313
  ///
314
  /// **Example:**
315
  /// ```dart
316
  /// final doc = YamlEditor('[0, 2]');
317
  /// doc.insertIntoList([], 1, 1); // [0, 1, 2]
318
  /// ```
319
  void insertIntoList(Iterable<Object?> path, int index, Object? value) {
39✔
320
    final valueNode = wrapAsYamlNode(value);
51✔
321

322
    final list = _traverseToList(path, checkAlias: true);
51✔
323
    RangeError.checkValueInInterval(index, 0, list.length);
78✔
324

325
    final edit = insertInList(this, list, index, valueNode);
51✔
326
    final expected = wrapAsYamlNode(
51✔
327
      [...list.nodes]..insert(index, valueNode),
105✔
328
    );
329

330
    _performEdit(edit, path, expected);
51✔
331
  }
12✔
332

333
  /// Changes the contents of the list at [path] by removing [deleteCount]
334
  /// items at [index], and inserting [values] in-place. Returns the elements
335
  /// that are deleted.
336
  ///
337
  /// [index] and [deleteCount] must be non-negative and [index] + [deleteCount]
338
  /// must be no greater than the list's length.
339
  ///
340
  /// Throws a [ArgumentError] if the element at the given path is not a
341
  /// [YamlList] or if the path is invalid.
342
  ///
343
  /// **Example:**
344
  /// ```dart
345
  /// final doc = YamlEditor('[Jan, March, April, June]');
346
  /// doc.spliceList([], 1, 0, ['Feb']); // [Jan, Feb, March, April, June]
347
  /// doc.spliceList([], 4, 1, ['May']); // [Jan, Feb, March, April, May]
348
  /// ```
349
  Iterable<YamlNode> spliceList(Iterable<Object?> path, int index,
21✔
350
      int deleteCount, Iterable<Object?> values) {
351
    final list = _traverseToList(path, checkAlias: true);
21✔
352

353
    RangeError.checkValueInInterval(index, 0, list.length);
33✔
354
    RangeError.checkValueInInterval(index + deleteCount, 0, list.length);
45✔
355

356
    final nodesToRemove = list.nodes.getRange(index, index + deleteCount);
45✔
357

358
    // Perform addition of elements before removal to avoid scenarios where
359
    // a block list gets emptied out to {} to avoid changing collection styles
360
    // where possible.
361

362
    // Reverse [values] and insert them.
363
    final reversedValues = values.toList().reversed;
27✔
364
    for (final value in reversedValues) {
33✔
365
      insertIntoList(path, index, value);
18✔
366
    }
367

368
    for (var i = 0; i < deleteCount; i++) {
33✔
369
      remove([...path, index + values.length]);
57✔
370
    }
371

372
    return nodesToRemove;
9✔
373
  }
9✔
374

375
  /// Removes the node at [path]. Comments "belonging" to the node will be
376
  /// removed while surrounding comments will be left untouched.
377
  ///
378
  /// Throws a [ArgumentError] if [path] is invalid.
379
  ///
380
  /// **Example:**
381
  /// ```dart
382
  /// final doc = YamlEditor('''
383
  /// - 0 # comment 0
384
  /// # comment A
385
  /// - 1 # comment 1
386
  /// # comment B
387
  /// - 2 # comment 2
388
  /// ''');
389
  /// doc.remove([1]);
390
  /// ```
391
  ///
392
  /// **Expected Result:**
393
  /// ```dart
394
  /// '''
395
  /// - 0 # comment 0
396
  /// # comment A
397
  /// # comment B
398
  /// - 2 # comment 2
399
  /// '''
400
  /// ```
401
  YamlNode remove(Iterable<Object?> path) {
45✔
402
    late SourceEdit edit;
21✔
403
    late YamlNode expectedNode;
21✔
404
    final nodeToRemove = _traverse(path, checkAlias: true);
45✔
405

406
    if (path.isEmpty) {
24✔
407
      edit = SourceEdit(0, _yaml.length, '');
12✔
408
      expectedNode = wrapAsYamlNode(null);
6✔
409

410
      /// Parsing an empty YAML document returns YamlScalar with value `null`.
411
      _performEdit(edit, path, expectedNode);
6✔
412
      return nodeToRemove;
3✔
413
    }
414

415
    final pathAsList = path.toList();
24✔
416
    final collectionPath = pathAsList.take(path.length - 1);
84✔
417
    final keyOrIndex = pathAsList.last;
45✔
418
    final parentNode = _traverse(collectionPath);
45✔
419

420
    if (parentNode is YamlList) {
45✔
421
      edit = removeInList(this, parentNode, keyOrIndex as int);
33✔
422
      expectedNode = wrapAsYamlNode(
33✔
423
        [...parentNode.nodes]..removeAt(keyOrIndex),
69✔
424
      );
425
    } else if (parentNode is YamlMap) {
39✔
426
      edit = removeInMap(this, parentNode, keyOrIndex);
39✔
427

428
      expectedNode =
429
          updatedYamlMap(parentNode, (nodes) => nodes.remove(keyOrIndex));
84✔
430
    }
431

432
    _performEdit(edit, collectionPath, expectedNode);
45✔
433

434
    return nodeToRemove;
21✔
435
  }
21✔
436

437
  /// Traverses down [path] to return the [YamlNode] at [path] if successful.
438
  ///
439
  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
440
  /// function is returned.
441
  ///
442
  /// If [orElse] is omitted, it defaults to throwing a [PathError].
443
  ///
444
  /// If [checkAlias] is `true`, throw [AliasError] if an aliased node is
445
  /// encountered.
446
  YamlNode _traverse(Iterable<Object?> path,
87✔
447
      {bool checkAlias = false, YamlNode Function()? orElse}) {
448
    if (path.isEmpty) return _contents;
132✔
449

450
    var currentNode = _contents;
81✔
451
    final pathList = path.toList();
69✔
452

453
    for (var i = 0; i < pathList.length; i++) {
165✔
454
      final keyOrIndex = pathList[i];
81✔
455

456
      if (checkAlias && _aliases.contains(currentNode)) {
114✔
457
        throw AliasError(path, currentNode);
×
458
      }
459

460
      if (currentNode is YamlList) {
81✔
461
        final list = currentNode;
462
        if (!isValidIndex(keyOrIndex, list.length)) {
87✔
463
          return _pathErrorOrElse(path, path.take(i + 1), list, orElse);
24✔
464
        }
465

466
        currentNode = list.nodes[keyOrIndex as int];
87✔
467
      } else if (currentNode is YamlMap) {
75✔
468
        final map = currentNode;
469

470
        if (!containsKey(map, keyOrIndex)) {
75✔
471
          return _pathErrorOrElse(path, path.take(i + 1), map, orElse);
24✔
472
        }
473
        final keyNode = getKeyNode(map, keyOrIndex);
75✔
474

475
        if (checkAlias) {
33✔
476
          if (_aliases.contains(keyNode)) throw AliasError(path, keyNode);
108✔
477
        }
478

479
        currentNode = map.nodes[keyNode]!;
114✔
480
      } else {
481
        return _pathErrorOrElse(path, path.take(i + 1), currentNode, orElse);
45✔
482
      }
483
    }
484

485
    if (checkAlias) _assertNoChildAlias(path, currentNode);
75✔
486

487
    return currentNode;
39✔
488
  }
42✔
489

490
  /// Throws a [PathError] if [orElse] is not provided, returns the result
491
  /// of invoking the [orElse] function otherwise.
492
  YamlNode _pathErrorOrElse(Iterable<Object?> path, Iterable<Object?> subPath,
9✔
493
      YamlNode parent, YamlNode Function()? orElse) {
494
    if (orElse == null) throw PathError(path, subPath, parent);
12✔
495
    return orElse();
6✔
496
  }
3✔
497

498
  /// Asserts that [node] and none its children are aliases
499
  void _assertNoChildAlias(Iterable<Object?> path, [YamlNode? node]) {
75✔
500
    if (node == null) return _assertNoChildAlias(path, _traverse(path));
36✔
501
    if (_aliases.contains(node)) throw AliasError(path, node);
117✔
502

503
    if (node is YamlScalar) return;
75✔
504

505
    if (node is YamlList) {
57✔
506
      for (var i = 0; i < node.length; i++) {
102✔
507
        final updatedPath = [...path, i];
78✔
508
        _assertNoChildAlias(updatedPath, node.nodes[i]);
105✔
509
      }
510
    }
511

512
    if (node is YamlMap) {
57✔
513
      final keyList = node.keys.toList();
69✔
514
      for (var i = 0; i < node.length; i++) {
90✔
515
        final updatedPath = [...path, keyList[i]];
69✔
516
        if (_aliases.contains(keyList[i])) {
93✔
NEW
517
          throw AliasError(path, keyList[i] as YamlNode);
×
518
        }
519
        _assertNoChildAlias(updatedPath, node.nodes[keyList[i]]);
117✔
520
      }
521
    }
522
  }
36✔
523

524
  /// Traverses down the provided [path] to return the [YamlList] at [path].
525
  ///
526
  /// Convenience function to ensure that a [YamlList] is returned.
527
  ///
528
  /// Throws [ArgumentError] if the element at the given path is not a
529
  /// [YamlList] or if the path is invalid. If [checkAlias] is `true`, and an
530
  /// aliased node is encountered along [path], an [AliasError] will be thrown.
531
  YamlList _traverseToList(Iterable<Object?> path, {bool checkAlias = false}) {
51✔
532
    final possibleList = _traverse(path, checkAlias: true);
51✔
533

534
    if (possibleList is YamlList) {
51✔
535
      return possibleList;
24✔
536
    } else {
537
      throw PathError.unexpected(
18✔
538
          path, 'Path $path does not point to a YamlList!');
18✔
539
    }
540
  }
24✔
541

542
  /// Utility method to replace the substring of [_yaml] according to [edit].
543
  ///
544
  /// When [_yaml] is modified with this method, the resulting string is parsed
545
  /// and reloaded and traversed down [path] to ensure that the reloaded YAML
546
  /// tree is equal to our expectations by deep equality of values. Throws an
547
  /// [AssertionError] if the two trees do not match.
548
  void _performEdit(
87✔
549
      SourceEdit edit, Iterable<Object?> path, YamlNode expectedNode) {
550
    final expectedTree = _deepModify(_contents, path, [], expectedNode);
177✔
551
    final initialYaml = _yaml;
87✔
552
    _yaml = edit.apply(_yaml);
177✔
553

554
    try {
555
      _initialize();
87✔
556
    } on YamlException {
557
      throw createAssertionError(
×
558
          'Failed to produce valid YAML after modification.',
559
          initialYaml,
560
          _yaml);
×
561
    }
562

563
    final actualTree = loadYamlNode(_yaml);
132✔
564
    if (!deepEquals(actualTree, expectedTree)) {
87✔
565
      throw createAssertionError(
×
566
          'Modification did not result in expected result.',
567
          initialYaml,
568
          _yaml);
×
569
    }
570

571
    _contents = actualTree;
87✔
572
    _edits.add(edit);
132✔
573
  }
42✔
574

575
  /// Utility method to produce an updated YAML tree equivalent to converting
576
  /// the [YamlNode] at [path] to be [expectedNode]. [subPath] holds the portion
577
  /// of [path] that has been traversed thus far.
578
  ///
579
  /// Throws a [PathError] if path is invalid.
580
  ///
581
  /// When called, it creates a new [YamlNode] of the same type as [tree], and
582
  /// copies its children over, except for the child that is on the path. Doing
583
  /// so allows us to "update" the immutable [YamlNode] without having to clone
584
  /// the whole tree.
585
  ///
586
  /// [SourceSpan]s in this new tree are not guaranteed to be accurate.
587
  YamlNode _deepModify(YamlNode tree, Iterable<Object?> path,
87✔
588
      Iterable<Object?> subPath, YamlNode expectedNode) {
589
    RangeError.checkValueInInterval(subPath.length, 0, path.length);
177✔
590

591
    if (path.length == subPath.length) return expectedNode;
177✔
592

593
    final keyOrIndex = path.elementAt(subPath.length);
78✔
594

595
    if (tree is YamlList) {
51✔
596
      if (!isValidIndex(keyOrIndex, tree.length)) {
36✔
597
        throw PathError(path, subPath, tree);
×
598
      }
599

600
      return wrapAsYamlNode([...tree.nodes]..[keyOrIndex as int] = _deepModify(
72✔
601
          tree.nodes[keyOrIndex],
36✔
602
          path,
603
          path.take(subPath.length + 1),
48✔
604
          expectedNode));
605
    }
606

607
    if (tree is YamlMap) {
51✔
608
      return updatedYamlMap(
51✔
609
          tree,
610
          (nodes) => nodes[keyOrIndex] = _deepModify(
105✔
611
              nodes[keyOrIndex] as YamlNode,
51✔
612
              path,
24✔
613
              path.take(subPath.length + 1),
105✔
614
              expectedNode));
24✔
615
    }
616

617
    /// Should not ever reach here.
618
    throw PathError(path, subPath, tree);
×
619
  }
42✔
620
}
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