@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.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
  }
@@ -897,12 +1092,17 @@ var Parser = class {
897
1092
  const segments = [];
898
1093
  let trailingDot = false;
899
1094
  let firstWasQuoted = false;
1095
+ let spaceConcat = false;
900
1096
  while (true) {
901
1097
  const t = this.peek();
902
1098
  if (t.kind === "string") {
903
1099
  this.advance();
904
1100
  if (segments.length === 0) firstWasQuoted = true;
905
- segments.push(t.value);
1101
+ if (spaceConcat) {
1102
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1103
+ } else {
1104
+ segments.push(t.value);
1105
+ }
906
1106
  trailingDot = false;
907
1107
  } else if (t.kind === "unquoted") {
908
1108
  this.advance();
@@ -919,12 +1119,23 @@ var Parser = class {
919
1119
  );
920
1120
  }
921
1121
  }
922
- segments.push(...filtered);
1122
+ if (spaceConcat && filtered.length > 0) {
1123
+ if (raw.startsWith(".")) {
1124
+ segments.push(...filtered);
1125
+ } else {
1126
+ const [head, ...tail] = filtered;
1127
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1128
+ segments.push(...tail);
1129
+ }
1130
+ } else {
1131
+ segments.push(...filtered);
1132
+ }
923
1133
  trailingDot = raw.endsWith(".");
924
1134
  } else {
925
1135
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
926
1136
  break;
927
1137
  }
1138
+ spaceConcat = false;
928
1139
  if (trailingDot) continue;
929
1140
  const next = this.peek();
930
1141
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
@@ -936,6 +1147,10 @@ var Parser = class {
936
1147
  trailingDot = true;
937
1148
  continue;
938
1149
  }
1150
+ if ((next.kind === "unquoted" || next.kind === "string") && next.precedingSpace) {
1151
+ spaceConcat = true;
1152
+ continue;
1153
+ }
939
1154
  break;
940
1155
  }
941
1156
  return { segments, firstWasQuoted };
@@ -1323,7 +1538,7 @@ var IncludeLoader = class {
1323
1538
  for (const ext of probeExts) {
1324
1539
  const obj = this.loadSingle(`${absPath}${ext}`);
1325
1540
  if (obj !== void 0) {
1326
- deepMergeResObjInto(merged, obj);
1541
+ deepMergeResObjInto(merged, obj, []);
1327
1542
  foundAny = true;
1328
1543
  }
1329
1544
  }
@@ -1356,7 +1571,7 @@ var IncludeLoader = class {
1356
1571
  for (const ext of probeExts) {
1357
1572
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1358
1573
  if (obj !== void 0) {
1359
- deepMergeResObjInto(merged, obj);
1574
+ deepMergeResObjInto(merged, obj, []);
1360
1575
  foundAny = true;
1361
1576
  }
1362
1577
  }
@@ -1528,7 +1743,7 @@ var StructureBuilder = class _StructureBuilder {
1528
1743
  if (pathPrefix.length > 0) {
1529
1744
  this.relativizeResObj(included, pathPrefix);
1530
1745
  }
1531
- deepMergeResObjInto(obj, included);
1746
+ deepMergeResObjInto(obj, included, pathPrefix);
1532
1747
  return;
1533
1748
  }
1534
1749
  const [head, ...tail] = field.key;
@@ -1543,12 +1758,27 @@ var StructureBuilder = class _StructureBuilder {
1543
1758
  return;
1544
1759
  }
1545
1760
  const childPrefix = [...pathPrefix, head];
1761
+ const fullKey = stringSegmentsToKey(childPrefix);
1546
1762
  if (field.append) {
1547
1763
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1548
- 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);
1549
1767
  obj.fields.set(head, {
1550
1768
  _kind: "append-placeholder",
1551
- 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,
1552
1782
  elem: this.astToResolverValue(field.value, childPrefix)
1553
1783
  });
1554
1784
  return;
@@ -1556,10 +1786,13 @@ var StructureBuilder = class _StructureBuilder {
1556
1786
  const existing = obj.fields.get(head);
1557
1787
  const newVal = this.astToResolverValue(field.value, childPrefix);
1558
1788
  if (existing !== void 0) {
1559
- 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);
1560
1793
  }
1561
1794
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1562
- deepMergeResObjInto(existing, newVal);
1795
+ deepMergeResObjInto(existing, newVal, childPrefix);
1563
1796
  return;
1564
1797
  }
1565
1798
  obj.fields.set(head, newVal);
@@ -1576,7 +1809,7 @@ var StructureBuilder = class _StructureBuilder {
1576
1809
  if (pathPrefix.length > 0) {
1577
1810
  this.relativizeResObj(included, pathPrefix);
1578
1811
  }
1579
- deepMergeResObjInto(obj, included);
1812
+ deepMergeResObjInto(obj, included, pathPrefix);
1580
1813
  return;
1581
1814
  }
1582
1815
  const [head, ...tail] = field.key;
@@ -1591,12 +1824,15 @@ var StructureBuilder = class _StructureBuilder {
1591
1824
  return;
1592
1825
  }
1593
1826
  const childPrefix = [...pathPrefix, head];
1827
+ const fullKey = stringSegmentsToKey(childPrefix);
1594
1828
  if (field.append) {
1595
1829
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1596
- 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);
1597
1833
  obj.fields.set(head, {
1598
1834
  _kind: "append-placeholder",
1599
- existing: existing2,
1835
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1600
1836
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1601
1837
  });
1602
1838
  return;
@@ -1604,10 +1840,13 @@ var StructureBuilder = class _StructureBuilder {
1604
1840
  const existing = obj.fields.get(head);
1605
1841
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1606
1842
  if (existing !== void 0) {
1607
- 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);
1608
1847
  }
1609
1848
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1610
- deepMergeResObjInto(existing, newVal);
1849
+ deepMergeResObjInto(existing, newVal, childPrefix);
1611
1850
  return;
1612
1851
  }
1613
1852
  obj.fields.set(head, newVal);
@@ -1739,6 +1978,23 @@ var SubstitutionResolver = class {
1739
1978
  // narrower than path-equality. Spec amendment deferred to a follow-up
1740
1979
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1741
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 = [];
1742
1998
  }
1743
1999
  /** Returns a prefix for error messages, including origin info when set. */
1744
2000
  originPrefix() {
@@ -1750,7 +2006,13 @@ var SubstitutionResolver = class {
1750
2006
  resolveResObj(obj) {
1751
2007
  const result = /* @__PURE__ */ new Map();
1752
2008
  for (const [key, val] of obj.fields) {
1753
- 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
+ }
1754
2016
  if (resolved !== void 0) {
1755
2017
  if (resolved.kind === "object") {
1756
2018
  const prior = obj.priorValues.get(key);
@@ -1840,7 +2102,10 @@ var SubstitutionResolver = class {
1840
2102
  try {
1841
2103
  const found = lookupPath(this.root, s.segments);
1842
2104
  if (found !== void 0) {
1843
- 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) {
1844
2109
  let prior;
1845
2110
  if (s.segments.length > 1) {
1846
2111
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -2051,8 +2316,33 @@ var SubstitutionResolver = class {
2051
2316
  }
2052
2317
  resolveAppend(a, scope) {
2053
2318
  const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2319
+ if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2320
+ return a;
2321
+ }
2054
2322
  const elem = this.resolveVal(a.elem, scope);
2055
- const items = existing.kind === "array" ? [...existing.items] : [existing];
2323
+ let priorItems;
2324
+ if (existing.kind === "array") {
2325
+ priorItems = existing.items;
2326
+ } else if (existing.kind === "object") {
2327
+ const converted = numericObjectToArray(existing);
2328
+ if (converted === null) {
2329
+ throw new ResolveError(
2330
+ `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2331
+ "",
2332
+ 0,
2333
+ 0
2334
+ );
2335
+ }
2336
+ priorItems = converted;
2337
+ } else {
2338
+ throw new ResolveError(
2339
+ `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2340
+ "",
2341
+ 0,
2342
+ 0
2343
+ );
2344
+ }
2345
+ const items = [...priorItems];
2056
2346
  if (elem !== void 0) items.push(elem);
2057
2347
  return { kind: "array", items };
2058
2348
  }
@@ -2154,6 +2444,9 @@ var Config = class _Config {
2154
2444
  }
2155
2445
  getString(path2) {
2156
2446
  const v = this.requireScalar(path2);
2447
+ if (v.valueType === "null") {
2448
+ throw new ConfigError(`expected string at ${path2}, got ${v.valueType}`, path2);
2449
+ }
2157
2450
  return v.raw;
2158
2451
  }
2159
2452
  getNumber(path2) {