@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.js CHANGED
@@ -286,6 +286,192 @@ function mergeUnresolved(receiver, fallback) {
286
286
  return result;
287
287
  }
288
288
 
289
+ // src/internal/resolver/fold-self-ref.ts
290
+ function substFullKey(s) {
291
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
292
+ }
293
+ function stringSegmentsToKey(segments) {
294
+ return segments.map((t) => {
295
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
296
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
297
+ }
298
+ return t;
299
+ }).join(".");
300
+ }
301
+ function containsSelfRef(v, fullKey) {
302
+ if (isSubst(v)) return substFullKey(v) === fullKey;
303
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
304
+ if (isAppend(v)) {
305
+ return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
306
+ }
307
+ if (isResObj(v)) {
308
+ for (const f of v.fields.values()) {
309
+ if (containsSelfRef(f, fullKey)) return true;
310
+ }
311
+ return false;
312
+ }
313
+ const hv = v;
314
+ if (hv.kind === "array") {
315
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
316
+ }
317
+ if (hv.kind === "object") {
318
+ for (const f of hv.fields.values()) {
319
+ if (containsSelfRef(f, fullKey)) return true;
320
+ }
321
+ return false;
322
+ }
323
+ return false;
324
+ }
325
+ function foldSelfRef(v, fullKey, replacement) {
326
+ if (isSubst(v)) {
327
+ return substFullKey(v) === fullKey ? replacement : v;
328
+ }
329
+ if (isConcat(v)) {
330
+ return {
331
+ _kind: "concat-placeholder",
332
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
333
+ line: v.line,
334
+ col: v.col
335
+ };
336
+ }
337
+ if (isAppend(v)) {
338
+ return {
339
+ _kind: "append-placeholder",
340
+ existing: foldSelfRef(v.existing, fullKey, replacement),
341
+ elem: foldSelfRef(v.elem, fullKey, replacement)
342
+ };
343
+ }
344
+ if (isResObj(v)) {
345
+ const newFields = /* @__PURE__ */ new Map();
346
+ for (const [k, val] of v.fields) {
347
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
348
+ }
349
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
350
+ }
351
+ const hv = v;
352
+ if (hv.kind === "array") {
353
+ return {
354
+ kind: "array",
355
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
356
+ };
357
+ }
358
+ if (hv.kind === "object") {
359
+ const newFields = /* @__PURE__ */ new Map();
360
+ for (const [k, val] of hv.fields) {
361
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
362
+ }
363
+ return { kind: "object", fields: newFields };
364
+ }
365
+ return v;
366
+ }
367
+ function foldOrSkipPrior(prior, fullKey, old) {
368
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
369
+ if (old === void 0) return void 0;
370
+ return foldSelfRef(prior, fullKey, old);
371
+ }
372
+ function cloneResolverValue(v) {
373
+ if (isSubst(v)) {
374
+ return {
375
+ _kind: "subst-placeholder",
376
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
377
+ optional: v.optional,
378
+ listSuffix: v.listSuffix,
379
+ line: v.line,
380
+ col: v.col,
381
+ prefixLen: v.prefixLen
382
+ };
383
+ }
384
+ if (isConcat(v)) {
385
+ return {
386
+ _kind: "concat-placeholder",
387
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
388
+ line: v.line,
389
+ col: v.col
390
+ };
391
+ }
392
+ if (isResObj(v)) {
393
+ const newFields = /* @__PURE__ */ new Map();
394
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
395
+ const newPriors = /* @__PURE__ */ new Map();
396
+ for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
397
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
398
+ }
399
+ if (isAppend(v)) {
400
+ return {
401
+ _kind: "append-placeholder",
402
+ existing: cloneResolverValue(v.existing),
403
+ elem: cloneResolverValue(v.elem)
404
+ };
405
+ }
406
+ const hv = v;
407
+ if (hv.kind === "array") {
408
+ return {
409
+ kind: "array",
410
+ items: hv.items.map((item) => cloneResolverValue(item))
411
+ };
412
+ }
413
+ if (hv.kind === "object") {
414
+ const newFields = /* @__PURE__ */ new Map();
415
+ for (const [k, val] of hv.fields) {
416
+ newFields.set(k, cloneResolverValue(val));
417
+ }
418
+ return { kind: "object", fields: newFields };
419
+ }
420
+ return hv;
421
+ }
422
+ function foldNestedSelfRefs(v, pathPrefix) {
423
+ if (!isResObj(v)) return v;
424
+ const newFields = /* @__PURE__ */ new Map();
425
+ for (const [k, fieldVal] of v.fields) {
426
+ const childPath = [...pathPrefix, k];
427
+ const fullKey = stringSegmentsToKey(childPath);
428
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
429
+ let finalVal = folded;
430
+ if (containsSelfRef(folded, fullKey)) {
431
+ const leafPrior = v.priorValues.get(k);
432
+ if (leafPrior !== void 0) {
433
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
434
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
435
+ }
436
+ }
437
+ newFields.set(k, finalVal);
438
+ }
439
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
440
+ }
441
+ function containsSubstByPath(v, target) {
442
+ if (isSubst(v)) return segmentsTextEqual(v.segments, target);
443
+ if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
444
+ if (isAppend(v)) {
445
+ return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
446
+ }
447
+ if (isResObj(v)) {
448
+ for (const f of v.fields.values()) {
449
+ if (containsSubstByPath(f, target)) return true;
450
+ }
451
+ return false;
452
+ }
453
+ const hv = v;
454
+ if (hv.kind === "array") {
455
+ return hv.items.some((item) => containsSubstByPath(item, target));
456
+ }
457
+ if (hv.kind === "object") {
458
+ for (const f of hv.fields.values()) {
459
+ if (containsSubstByPath(f, target)) return true;
460
+ }
461
+ return false;
462
+ }
463
+ return false;
464
+ }
465
+ function segmentsTextEqual(a, b) {
466
+ if (a.length !== b.length) return false;
467
+ for (let i = 0; i < a.length; i++) {
468
+ const ai = a[i];
469
+ const bi = b[i];
470
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
471
+ }
472
+ return true;
473
+ }
474
+
289
475
  // src/internal/resolver/utils.ts
290
476
  function segmentsToKey(segments) {
291
477
  return segments.map((s) => {
@@ -327,13 +513,22 @@ function deepMergeHoconValues(base, overlay) {
327
513
  }
328
514
  return { kind: "object", fields: merged };
329
515
  }
330
- function deepMergeResObjInto(dst, src) {
516
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
331
517
  for (const [k, srcVal] of src.fields) {
518
+ const childPrefix = [...pathPrefix, k];
519
+ const fullKey = stringSegmentsToKey(childPrefix);
332
520
  const dstVal = dst.fields.get(k);
333
521
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
334
- deepMergeResObjInto(dstVal, srcVal);
522
+ const priorExisting = dst.priorValues.get(k);
523
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
524
+ if (prior !== void 0) dst.priorValues.set(k, prior);
525
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
335
526
  } else {
336
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
527
+ if (dstVal !== void 0) {
528
+ const priorExisting = dst.priorValues.get(k);
529
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
530
+ if (prior !== void 0) dst.priorValues.set(k, prior);
531
+ }
337
532
  dst.fields.set(k, srcVal);
338
533
  }
339
534
  }
@@ -1343,7 +1538,7 @@ var IncludeLoader = class {
1343
1538
  for (const ext of probeExts) {
1344
1539
  const obj = this.loadSingle(`${absPath}${ext}`);
1345
1540
  if (obj !== void 0) {
1346
- deepMergeResObjInto(merged, obj);
1541
+ deepMergeResObjInto(merged, obj, []);
1347
1542
  foundAny = true;
1348
1543
  }
1349
1544
  }
@@ -1376,7 +1571,7 @@ var IncludeLoader = class {
1376
1571
  for (const ext of probeExts) {
1377
1572
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1378
1573
  if (obj !== void 0) {
1379
- deepMergeResObjInto(merged, obj);
1574
+ deepMergeResObjInto(merged, obj, []);
1380
1575
  foundAny = true;
1381
1576
  }
1382
1577
  }
@@ -1548,7 +1743,7 @@ var StructureBuilder = class _StructureBuilder {
1548
1743
  if (pathPrefix.length > 0) {
1549
1744
  this.relativizeResObj(included, pathPrefix);
1550
1745
  }
1551
- deepMergeResObjInto(obj, included);
1746
+ deepMergeResObjInto(obj, included, pathPrefix);
1552
1747
  return;
1553
1748
  }
1554
1749
  const [head, ...tail] = field.key;
@@ -1563,12 +1758,27 @@ var StructureBuilder = class _StructureBuilder {
1563
1758
  return;
1564
1759
  }
1565
1760
  const childPrefix = [...pathPrefix, head];
1761
+ const fullKey = stringSegmentsToKey(childPrefix);
1566
1762
  if (field.append) {
1567
1763
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1568
- obj.priorValues.set(head, existing2);
1764
+ const oldPrior = obj.priorValues.get(head);
1765
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1766
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1569
1767
  obj.fields.set(head, {
1570
1768
  _kind: "append-placeholder",
1571
- existing: existing2,
1769
+ // Use the folded existing (self-ref-free) for the Append. When the
1770
+ // fold returned undefined (self-ref present but no old prior),
1771
+ // fall back to the original existing — resolveSubst will then
1772
+ // raise the "self-ref with no prior value" error at resolve time,
1773
+ // matching the pre-fix behaviour for that edge case.
1774
+ //
1775
+ // Clone `folded` again so the Append.existing and priorValues[head]
1776
+ // do not share the same Subst/Concat references — relativizeResObj
1777
+ // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1778
+ // in place, so a shared reference would get its prefix applied
1779
+ // twice (resulting in e.g. `${outer.outer.base}` instead of
1780
+ // `${outer.base}`). Codex review on PR #131.
1781
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1572
1782
  elem: this.astToResolverValue(field.value, childPrefix)
1573
1783
  });
1574
1784
  return;
@@ -1576,10 +1786,13 @@ var StructureBuilder = class _StructureBuilder {
1576
1786
  const existing = obj.fields.get(head);
1577
1787
  const newVal = this.astToResolverValue(field.value, childPrefix);
1578
1788
  if (existing !== void 0) {
1579
- obj.priorValues.set(head, existing);
1789
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1790
+ const oldPrior = obj.priorValues.get(head);
1791
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1792
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1580
1793
  }
1581
1794
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1582
- deepMergeResObjInto(existing, newVal);
1795
+ deepMergeResObjInto(existing, newVal, childPrefix);
1583
1796
  return;
1584
1797
  }
1585
1798
  obj.fields.set(head, newVal);
@@ -1596,7 +1809,7 @@ var StructureBuilder = class _StructureBuilder {
1596
1809
  if (pathPrefix.length > 0) {
1597
1810
  this.relativizeResObj(included, pathPrefix);
1598
1811
  }
1599
- deepMergeResObjInto(obj, included);
1812
+ deepMergeResObjInto(obj, included, pathPrefix);
1600
1813
  return;
1601
1814
  }
1602
1815
  const [head, ...tail] = field.key;
@@ -1611,12 +1824,15 @@ var StructureBuilder = class _StructureBuilder {
1611
1824
  return;
1612
1825
  }
1613
1826
  const childPrefix = [...pathPrefix, head];
1827
+ const fullKey = stringSegmentsToKey(childPrefix);
1614
1828
  if (field.append) {
1615
1829
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1616
- obj.priorValues.set(head, existing2);
1830
+ const oldPrior = obj.priorValues.get(head);
1831
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1832
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1617
1833
  obj.fields.set(head, {
1618
1834
  _kind: "append-placeholder",
1619
- existing: existing2,
1835
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1620
1836
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1621
1837
  });
1622
1838
  return;
@@ -1624,10 +1840,13 @@ var StructureBuilder = class _StructureBuilder {
1624
1840
  const existing = obj.fields.get(head);
1625
1841
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1626
1842
  if (existing !== void 0) {
1627
- obj.priorValues.set(head, existing);
1843
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1844
+ const oldPrior = obj.priorValues.get(head);
1845
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1846
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1628
1847
  }
1629
1848
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1630
- deepMergeResObjInto(existing, newVal);
1849
+ deepMergeResObjInto(existing, newVal, childPrefix);
1631
1850
  return;
1632
1851
  }
1633
1852
  obj.fields.set(head, newVal);
@@ -1759,6 +1978,23 @@ var SubstitutionResolver = class {
1759
1978
  // narrower than path-equality. Spec amendment deferred to a follow-up
1760
1979
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1761
1980
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1981
+ // Path stack tracking the full dotted path of the field currently being
1982
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
1983
+ // `(key, val)` iteration and popped on exit, so nested objects build up
1984
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
1985
+ //
1986
+ // Used to tighten self-ref detection at the L192 found-lookup site:
1987
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
1988
+ // at `X` is only a true self-reference when the field currently being
1989
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
1990
+ // external lookup like `b = ${o}` (where o has a self-referential
1991
+ // field) would mis-fire and short-circuit to o's prior, returning the
1992
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
1993
+ // narrows the check to the owner case only.
1994
+ //
1995
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
1996
+ // same prefix-match semantics).
1997
+ this.resolvingFieldPath = [];
1762
1998
  }
1763
1999
  /** Returns a prefix for error messages, including origin info when set. */
1764
2000
  originPrefix() {
@@ -1770,7 +2006,13 @@ var SubstitutionResolver = class {
1770
2006
  resolveResObj(obj) {
1771
2007
  const result = /* @__PURE__ */ new Map();
1772
2008
  for (const [key, val] of obj.fields) {
1773
- const resolved = this.resolveVal(val, obj);
2009
+ this.resolvingFieldPath.push(key);
2010
+ let resolved;
2011
+ try {
2012
+ resolved = this.resolveVal(val, obj);
2013
+ } finally {
2014
+ this.resolvingFieldPath.pop();
2015
+ }
1774
2016
  if (resolved !== void 0) {
1775
2017
  if (resolved.kind === "object") {
1776
2018
  const prior = obj.priorValues.get(key);
@@ -1860,7 +2102,10 @@ var SubstitutionResolver = class {
1860
2102
  try {
1861
2103
  const found = lookupPath(this.root, s.segments);
1862
2104
  if (found !== void 0) {
1863
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2105
+ const rfp = this.resolvingFieldPath;
2106
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2107
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2108
+ if (isSelfRef) {
1864
2109
  let prior;
1865
2110
  if (s.segments.length > 1) {
1866
2111
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";