@o3co/ts.hocon 1.4.1 → 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
  }
@@ -949,12 +1144,17 @@ var Parser = class {
949
1144
  const segments = [];
950
1145
  let trailingDot = false;
951
1146
  let firstWasQuoted = false;
1147
+ let spaceConcat = false;
952
1148
  while (true) {
953
1149
  const t = this.peek();
954
1150
  if (t.kind === "string") {
955
1151
  this.advance();
956
1152
  if (segments.length === 0) firstWasQuoted = true;
957
- segments.push(t.value);
1153
+ if (spaceConcat) {
1154
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1155
+ } else {
1156
+ segments.push(t.value);
1157
+ }
958
1158
  trailingDot = false;
959
1159
  } else if (t.kind === "unquoted") {
960
1160
  this.advance();
@@ -971,12 +1171,23 @@ var Parser = class {
971
1171
  );
972
1172
  }
973
1173
  }
974
- segments.push(...filtered);
1174
+ if (spaceConcat && filtered.length > 0) {
1175
+ if (raw.startsWith(".")) {
1176
+ segments.push(...filtered);
1177
+ } else {
1178
+ const [head, ...tail] = filtered;
1179
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1180
+ segments.push(...tail);
1181
+ }
1182
+ } else {
1183
+ segments.push(...filtered);
1184
+ }
975
1185
  trailingDot = raw.endsWith(".");
976
1186
  } else {
977
1187
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
978
1188
  break;
979
1189
  }
1190
+ spaceConcat = false;
980
1191
  if (trailingDot) continue;
981
1192
  const next = this.peek();
982
1193
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
@@ -988,6 +1199,10 @@ var Parser = class {
988
1199
  trailingDot = true;
989
1200
  continue;
990
1201
  }
1202
+ if ((next.kind === "unquoted" || next.kind === "string") && next.precedingSpace) {
1203
+ spaceConcat = true;
1204
+ continue;
1205
+ }
991
1206
  break;
992
1207
  }
993
1208
  return { segments, firstWasQuoted };
@@ -1376,7 +1591,7 @@ var IncludeLoader = class {
1376
1591
  for (const ext of probeExts) {
1377
1592
  const obj = this.loadSingle(`${absPath}${ext}`);
1378
1593
  if (obj !== void 0) {
1379
- deepMergeResObjInto(merged, obj);
1594
+ deepMergeResObjInto(merged, obj, []);
1380
1595
  foundAny = true;
1381
1596
  }
1382
1597
  }
@@ -1409,7 +1624,7 @@ var IncludeLoader = class {
1409
1624
  for (const ext of probeExts) {
1410
1625
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1411
1626
  if (obj !== void 0) {
1412
- deepMergeResObjInto(merged, obj);
1627
+ deepMergeResObjInto(merged, obj, []);
1413
1628
  foundAny = true;
1414
1629
  }
1415
1630
  }
@@ -1581,7 +1796,7 @@ var StructureBuilder = class _StructureBuilder {
1581
1796
  if (pathPrefix.length > 0) {
1582
1797
  this.relativizeResObj(included, pathPrefix);
1583
1798
  }
1584
- deepMergeResObjInto(obj, included);
1799
+ deepMergeResObjInto(obj, included, pathPrefix);
1585
1800
  return;
1586
1801
  }
1587
1802
  const [head, ...tail] = field.key;
@@ -1596,12 +1811,27 @@ var StructureBuilder = class _StructureBuilder {
1596
1811
  return;
1597
1812
  }
1598
1813
  const childPrefix = [...pathPrefix, head];
1814
+ const fullKey = stringSegmentsToKey(childPrefix);
1599
1815
  if (field.append) {
1600
1816
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1601
- 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);
1602
1820
  obj.fields.set(head, {
1603
1821
  _kind: "append-placeholder",
1604
- 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,
1605
1835
  elem: this.astToResolverValue(field.value, childPrefix)
1606
1836
  });
1607
1837
  return;
@@ -1609,10 +1839,13 @@ var StructureBuilder = class _StructureBuilder {
1609
1839
  const existing = obj.fields.get(head);
1610
1840
  const newVal = this.astToResolverValue(field.value, childPrefix);
1611
1841
  if (existing !== void 0) {
1612
- 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);
1613
1846
  }
1614
1847
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1615
- deepMergeResObjInto(existing, newVal);
1848
+ deepMergeResObjInto(existing, newVal, childPrefix);
1616
1849
  return;
1617
1850
  }
1618
1851
  obj.fields.set(head, newVal);
@@ -1629,7 +1862,7 @@ var StructureBuilder = class _StructureBuilder {
1629
1862
  if (pathPrefix.length > 0) {
1630
1863
  this.relativizeResObj(included, pathPrefix);
1631
1864
  }
1632
- deepMergeResObjInto(obj, included);
1865
+ deepMergeResObjInto(obj, included, pathPrefix);
1633
1866
  return;
1634
1867
  }
1635
1868
  const [head, ...tail] = field.key;
@@ -1644,12 +1877,15 @@ var StructureBuilder = class _StructureBuilder {
1644
1877
  return;
1645
1878
  }
1646
1879
  const childPrefix = [...pathPrefix, head];
1880
+ const fullKey = stringSegmentsToKey(childPrefix);
1647
1881
  if (field.append) {
1648
1882
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1649
- 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);
1650
1886
  obj.fields.set(head, {
1651
1887
  _kind: "append-placeholder",
1652
- existing: existing2,
1888
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1653
1889
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1654
1890
  });
1655
1891
  return;
@@ -1657,10 +1893,13 @@ var StructureBuilder = class _StructureBuilder {
1657
1893
  const existing = obj.fields.get(head);
1658
1894
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1659
1895
  if (existing !== void 0) {
1660
- 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);
1661
1900
  }
1662
1901
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1663
- deepMergeResObjInto(existing, newVal);
1902
+ deepMergeResObjInto(existing, newVal, childPrefix);
1664
1903
  return;
1665
1904
  }
1666
1905
  obj.fields.set(head, newVal);
@@ -1792,6 +2031,23 @@ var SubstitutionResolver = class {
1792
2031
  // narrower than path-equality. Spec amendment deferred to a follow-up
1793
2032
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1794
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 = [];
1795
2051
  }
1796
2052
  /** Returns a prefix for error messages, including origin info when set. */
1797
2053
  originPrefix() {
@@ -1803,7 +2059,13 @@ var SubstitutionResolver = class {
1803
2059
  resolveResObj(obj) {
1804
2060
  const result = /* @__PURE__ */ new Map();
1805
2061
  for (const [key, val] of obj.fields) {
1806
- 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
+ }
1807
2069
  if (resolved !== void 0) {
1808
2070
  if (resolved.kind === "object") {
1809
2071
  const prior = obj.priorValues.get(key);
@@ -1893,7 +2155,10 @@ var SubstitutionResolver = class {
1893
2155
  try {
1894
2156
  const found = lookupPath(this.root, s.segments);
1895
2157
  if (found !== void 0) {
1896
- 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) {
1897
2162
  let prior;
1898
2163
  if (s.segments.length > 1) {
1899
2164
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -2104,8 +2369,33 @@ var SubstitutionResolver = class {
2104
2369
  }
2105
2370
  resolveAppend(a, scope) {
2106
2371
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2372
+ if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2373
+ return a;
2374
+ }
2107
2375
  const elem = this.resolveVal(a.elem, scope);
2108
- const items = existing.kind === "array" ? [...existing.items] : [existing];
2376
+ let priorItems;
2377
+ if (existing.kind === "array") {
2378
+ priorItems = existing.items;
2379
+ } else if (existing.kind === "object") {
2380
+ const converted = numericObjectToArray(existing);
2381
+ if (converted === null) {
2382
+ throw new ResolveError(
2383
+ `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2384
+ "",
2385
+ 0,
2386
+ 0
2387
+ );
2388
+ }
2389
+ priorItems = converted;
2390
+ } else {
2391
+ throw new ResolveError(
2392
+ `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2393
+ "",
2394
+ 0,
2395
+ 0
2396
+ );
2397
+ }
2398
+ const items = [...priorItems];
2109
2399
  if (elem !== void 0) items.push(elem);
2110
2400
  return { kind: "array", items };
2111
2401
  }
@@ -2207,6 +2497,9 @@ var Config = class _Config {
2207
2497
  }
2208
2498
  getString(path2) {
2209
2499
  const v = this.requireScalar(path2);
2500
+ if (v.valueType === "null") {
2501
+ throw new ConfigError(`expected string at ${path2}, got ${v.valueType}`, path2);
2502
+ }
2210
2503
  return v.raw;
2211
2504
  }
2212
2505
  getNumber(path2) {