@o3co/ts.hocon 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -338,6 +338,192 @@ function mergeUnresolved(receiver, fallback) {
338
338
  return result;
339
339
  }
340
340
 
341
+ // src/internal/resolver/fold-self-ref.ts
342
+ function substFullKey(s) {
343
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
344
+ }
345
+ function stringSegmentsToKey(segments) {
346
+ return segments.map((t) => {
347
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
348
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
349
+ }
350
+ return t;
351
+ }).join(".");
352
+ }
353
+ function containsSelfRef(v, fullKey) {
354
+ if (isSubst(v)) return substFullKey(v) === fullKey;
355
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
356
+ if (isAppend(v)) {
357
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
358
+ }
359
+ if (isResObj(v)) {
360
+ for (const f of v.fields.values()) {
361
+ if (containsSelfRef(f, fullKey)) return true;
362
+ }
363
+ return false;
364
+ }
365
+ const hv = v;
366
+ if (hv.kind === "array") {
367
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
368
+ }
369
+ if (hv.kind === "object") {
370
+ for (const f of hv.fields.values()) {
371
+ if (containsSelfRef(f, fullKey)) return true;
372
+ }
373
+ return false;
374
+ }
375
+ return false;
376
+ }
377
+ function foldSelfRef(v, fullKey, replacement) {
378
+ if (isSubst(v)) {
379
+ return substFullKey(v) === fullKey ? replacement : v;
380
+ }
381
+ if (isConcat(v)) {
382
+ return {
383
+ _kind: "concat-placeholder",
384
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
385
+ line: v.line,
386
+ col: v.col
387
+ };
388
+ }
389
+ if (isAppend(v)) {
390
+ return {
391
+ _kind: "append-placeholder",
392
+ existing: foldSelfRef(v.existing, fullKey, replacement),
393
+ elem: foldSelfRef(v.elem, fullKey, replacement)
394
+ };
395
+ }
396
+ if (isResObj(v)) {
397
+ const newFields = /* @__PURE__ */ new Map();
398
+ for (const [k, val] of v.fields) {
399
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
400
+ }
401
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
402
+ }
403
+ const hv = v;
404
+ if (hv.kind === "array") {
405
+ return {
406
+ kind: "array",
407
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
408
+ };
409
+ }
410
+ if (hv.kind === "object") {
411
+ const newFields = /* @__PURE__ */ new Map();
412
+ for (const [k, val] of hv.fields) {
413
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
414
+ }
415
+ return { kind: "object", fields: newFields };
416
+ }
417
+ return v;
418
+ }
419
+ function foldOrSkipPrior(prior, fullKey, old) {
420
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
421
+ if (old === void 0) return void 0;
422
+ return foldSelfRef(prior, fullKey, old);
423
+ }
424
+ function cloneResolverValue(v) {
425
+ if (isSubst(v)) {
426
+ return {
427
+ _kind: "subst-placeholder",
428
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
429
+ optional: v.optional,
430
+ listSuffix: v.listSuffix,
431
+ line: v.line,
432
+ col: v.col,
433
+ prefixLen: v.prefixLen
434
+ };
435
+ }
436
+ if (isConcat(v)) {
437
+ return {
438
+ _kind: "concat-placeholder",
439
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
440
+ line: v.line,
441
+ col: v.col
442
+ };
443
+ }
444
+ if (isResObj(v)) {
445
+ const newFields = /* @__PURE__ */ new Map();
446
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
447
+ const newPriors = /* @__PURE__ */ new Map();
448
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
449
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
450
+ }
451
+ if (isAppend(v)) {
452
+ return {
453
+ _kind: "append-placeholder",
454
+ existing: cloneResolverValue(v.existing),
455
+ elem: cloneResolverValue(v.elem)
456
+ };
457
+ }
458
+ const hv = v;
459
+ if (hv.kind === "array") {
460
+ return {
461
+ kind: "array",
462
+ items: hv.items.map((item) => cloneResolverValue(item))
463
+ };
464
+ }
465
+ if (hv.kind === "object") {
466
+ const newFields = /* @__PURE__ */ new Map();
467
+ for (const [k, val] of hv.fields) {
468
+ newFields.set(k, cloneResolverValue(val));
469
+ }
470
+ return { kind: "object", fields: newFields };
471
+ }
472
+ return hv;
473
+ }
474
+ function foldNestedSelfRefs(v, pathPrefix) {
475
+ if (!isResObj(v)) return v;
476
+ const newFields = /* @__PURE__ */ new Map();
477
+ for (const [k, fieldVal] of v.fields) {
478
+ const childPath = [...pathPrefix, k];
479
+ const fullKey = stringSegmentsToKey(childPath);
480
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
481
+ let finalVal = folded;
482
+ if (containsSelfRef(folded, fullKey)) {
483
+ const leafPrior = v.priorValues.get(k);
484
+ if (leafPrior !== void 0) {
485
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
486
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
487
+ }
488
+ }
489
+ newFields.set(k, finalVal);
490
+ }
491
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
492
+ }
493
+ function containsSubstByPath(v, target) {
494
+ if (isSubst(v)) return segmentsTextEqual(v.segments, target);
495
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
496
+ if (isAppend(v)) {
497
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
498
+ }
499
+ if (isResObj(v)) {
500
+ for (const f of v.fields.values()) {
501
+ if (containsSubstByPath(f, target)) return true;
502
+ }
503
+ return false;
504
+ }
505
+ const hv = v;
506
+ if (hv.kind === "array") {
507
+ return hv.items.some((item) => containsSubstByPath(item, target));
508
+ }
509
+ if (hv.kind === "object") {
510
+ for (const f of hv.fields.values()) {
511
+ if (containsSubstByPath(f, target)) return true;
512
+ }
513
+ return false;
514
+ }
515
+ return false;
516
+ }
517
+ function segmentsTextEqual(a, b) {
518
+ if (a.length !== b.length) return false;
519
+ for (let i = 0; i < a.length; i++) {
520
+ const ai = a[i];
521
+ const bi = b[i];
522
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
523
+ }
524
+ return true;
525
+ }
526
+
341
527
  // src/internal/resolver/utils.ts
342
528
  function segmentsToKey(segments) {
343
529
  return segments.map((s) => {
@@ -379,13 +565,22 @@ function deepMergeHoconValues(base, overlay) {
379
565
  }
380
566
  return { kind: "object", fields: merged };
381
567
  }
382
- function deepMergeResObjInto(dst, src) {
568
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
383
569
  for (const [k, srcVal] of src.fields) {
570
+ const childPrefix = [...pathPrefix, k];
571
+ const fullKey = stringSegmentsToKey(childPrefix);
384
572
  const dstVal = dst.fields.get(k);
385
573
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
386
- deepMergeResObjInto(dstVal, srcVal);
574
+ const priorExisting = dst.priorValues.get(k);
575
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
576
+ if (prior !== void 0) dst.priorValues.set(k, prior);
577
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
387
578
  } else {
388
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
579
+ if (dstVal !== void 0) {
580
+ const priorExisting = dst.priorValues.get(k);
581
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
582
+ if (prior !== void 0) dst.priorValues.set(k, prior);
583
+ }
389
584
  dst.fields.set(k, srcVal);
390
585
  }
391
586
  }
@@ -1396,7 +1591,7 @@ var IncludeLoader = class {
1396
1591
  for (const ext of probeExts) {
1397
1592
  const obj = this.loadSingle(`${absPath}${ext}`);
1398
1593
  if (obj !== void 0) {
1399
- deepMergeResObjInto(merged, obj);
1594
+ deepMergeResObjInto(merged, obj, []);
1400
1595
  foundAny = true;
1401
1596
  }
1402
1597
  }
@@ -1429,7 +1624,7 @@ var IncludeLoader = class {
1429
1624
  for (const ext of probeExts) {
1430
1625
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1431
1626
  if (obj !== void 0) {
1432
- deepMergeResObjInto(merged, obj);
1627
+ deepMergeResObjInto(merged, obj, []);
1433
1628
  foundAny = true;
1434
1629
  }
1435
1630
  }
@@ -1601,7 +1796,7 @@ var StructureBuilder = class _StructureBuilder {
1601
1796
  if (pathPrefix.length > 0) {
1602
1797
  this.relativizeResObj(included, pathPrefix);
1603
1798
  }
1604
- deepMergeResObjInto(obj, included);
1799
+ deepMergeResObjInto(obj, included, pathPrefix);
1605
1800
  return;
1606
1801
  }
1607
1802
  const [head, ...tail] = field.key;
@@ -1616,12 +1811,27 @@ var StructureBuilder = class _StructureBuilder {
1616
1811
  return;
1617
1812
  }
1618
1813
  const childPrefix = [...pathPrefix, head];
1814
+ const fullKey = stringSegmentsToKey(childPrefix);
1619
1815
  if (field.append) {
1620
1816
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1621
- obj.priorValues.set(head, existing2);
1817
+ const oldPrior = obj.priorValues.get(head);
1818
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1819
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1622
1820
  obj.fields.set(head, {
1623
1821
  _kind: "append-placeholder",
1624
- existing: existing2,
1822
+ // Use the folded existing (self-ref-free) for the Append. When the
1823
+ // fold returned undefined (self-ref present but no old prior),
1824
+ // fall back to the original existing — resolveSubst will then
1825
+ // raise the "self-ref with no prior value" error at resolve time,
1826
+ // matching the pre-fix behaviour for that edge case.
1827
+ //
1828
+ // Clone `folded` again so the Append.existing and priorValues[head]
1829
+ // do not share the same Subst/Concat references — relativizeResObj
1830
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1831
+ // in place, so a shared reference would get its prefix applied
1832
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1833
+ // `${outer.base}`). Codex review on PR #131.
1834
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1625
1835
  elem: this.astToResolverValue(field.value, childPrefix)
1626
1836
  });
1627
1837
  return;
@@ -1629,10 +1839,13 @@ var StructureBuilder = class _StructureBuilder {
1629
1839
  const existing = obj.fields.get(head);
1630
1840
  const newVal = this.astToResolverValue(field.value, childPrefix);
1631
1841
  if (existing !== void 0) {
1632
- obj.priorValues.set(head, existing);
1842
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1843
+ const oldPrior = obj.priorValues.get(head);
1844
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1845
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1633
1846
  }
1634
1847
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1635
- deepMergeResObjInto(existing, newVal);
1848
+ deepMergeResObjInto(existing, newVal, childPrefix);
1636
1849
  return;
1637
1850
  }
1638
1851
  obj.fields.set(head, newVal);
@@ -1649,7 +1862,7 @@ var StructureBuilder = class _StructureBuilder {
1649
1862
  if (pathPrefix.length > 0) {
1650
1863
  this.relativizeResObj(included, pathPrefix);
1651
1864
  }
1652
- deepMergeResObjInto(obj, included);
1865
+ deepMergeResObjInto(obj, included, pathPrefix);
1653
1866
  return;
1654
1867
  }
1655
1868
  const [head, ...tail] = field.key;
@@ -1664,12 +1877,15 @@ var StructureBuilder = class _StructureBuilder {
1664
1877
  return;
1665
1878
  }
1666
1879
  const childPrefix = [...pathPrefix, head];
1880
+ const fullKey = stringSegmentsToKey(childPrefix);
1667
1881
  if (field.append) {
1668
1882
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1669
- obj.priorValues.set(head, existing2);
1883
+ const oldPrior = obj.priorValues.get(head);
1884
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1885
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1670
1886
  obj.fields.set(head, {
1671
1887
  _kind: "append-placeholder",
1672
- existing: existing2,
1888
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1673
1889
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1674
1890
  });
1675
1891
  return;
@@ -1677,10 +1893,13 @@ var StructureBuilder = class _StructureBuilder {
1677
1893
  const existing = obj.fields.get(head);
1678
1894
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1679
1895
  if (existing !== void 0) {
1680
- obj.priorValues.set(head, existing);
1896
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1897
+ const oldPrior = obj.priorValues.get(head);
1898
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1899
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1681
1900
  }
1682
1901
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1683
- deepMergeResObjInto(existing, newVal);
1902
+ deepMergeResObjInto(existing, newVal, childPrefix);
1684
1903
  return;
1685
1904
  }
1686
1905
  obj.fields.set(head, newVal);
@@ -1812,6 +2031,23 @@ var SubstitutionResolver = class {
1812
2031
  // narrower than path-equality. Spec amendment deferred to a follow-up
1813
2032
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1814
2033
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
2034
+ // Path stack tracking the full dotted path of the field currently being
2035
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
2036
+ // `(key, val)` iteration and popped on exit, so nested objects build up
2037
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
2038
+ //
2039
+ // Used to tighten self-ref detection at the L192 found-lookup site:
2040
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
2041
+ // at `X` is only a true self-reference when the field currently being
2042
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
2043
+ // external lookup like `b = ${o}` (where o has a self-referential
2044
+ // field) would mis-fire and short-circuit to o's prior, returning the
2045
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
2046
+ // narrows the check to the owner case only.
2047
+ //
2048
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2049
+ // same prefix-match semantics).
2050
+ this.resolvingFieldPath = [];
1815
2051
  }
1816
2052
  /** Returns a prefix for error messages, including origin info when set. */
1817
2053
  originPrefix() {
@@ -1823,7 +2059,13 @@ var SubstitutionResolver = class {
1823
2059
  resolveResObj(obj) {
1824
2060
  const result = /* @__PURE__ */ new Map();
1825
2061
  for (const [key, val] of obj.fields) {
1826
- const resolved = this.resolveVal(val, obj);
2062
+ this.resolvingFieldPath.push(key);
2063
+ let resolved;
2064
+ try {
2065
+ resolved = this.resolveVal(val, obj);
2066
+ } finally {
2067
+ this.resolvingFieldPath.pop();
2068
+ }
1827
2069
  if (resolved !== void 0) {
1828
2070
  if (resolved.kind === "object") {
1829
2071
  const prior = obj.priorValues.get(key);
@@ -1913,7 +2155,10 @@ var SubstitutionResolver = class {
1913
2155
  try {
1914
2156
  const found = lookupPath(this.root, s.segments);
1915
2157
  if (found !== void 0) {
1916
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2158
+ const rfp = this.resolvingFieldPath;
2159
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2160
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2161
+ if (isSelfRef) {
1917
2162
  let prior;
1918
2163
  if (s.segments.length > 1) {
1919
2164
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";