@o3co/ts.hocon 1.2.0 → 1.3.0
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/README.ja.md +4 -2
- package/README.md +4 -2
- package/dist/index.cjs +307 -73
- package/dist/index.js +307 -73
- package/dist/zod.cjs +307 -73
- package/dist/zod.js +307 -73
- package/package.json +1 -1
package/dist/zod.cjs
CHANGED
|
@@ -39,6 +39,27 @@ module.exports = __toCommonJS(zod_exports);
|
|
|
39
39
|
|
|
40
40
|
// src/coerce.ts
|
|
41
41
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
42
|
+
function isHoconWs(cp) {
|
|
43
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
44
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
45
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
46
|
+
if (cp === 5760) return true;
|
|
47
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
48
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
49
|
+
if (cp === 12288) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function trimHoconWs(s) {
|
|
53
|
+
let start = 0;
|
|
54
|
+
let end = s.length;
|
|
55
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
56
|
+
start++;
|
|
57
|
+
}
|
|
58
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
59
|
+
end--;
|
|
60
|
+
}
|
|
61
|
+
return s.slice(start, end);
|
|
62
|
+
}
|
|
42
63
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
43
64
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
44
65
|
function coerceBoolean(value) {
|
|
@@ -84,22 +105,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
84
105
|
d: 864e5
|
|
85
106
|
};
|
|
86
107
|
function parseDuration(value, outputUnit = "ms") {
|
|
87
|
-
const trimmed = value
|
|
108
|
+
const trimmed = trimHoconWs(value);
|
|
88
109
|
let i = 0;
|
|
89
110
|
while (i < trimmed.length) {
|
|
90
111
|
const ch = trimmed[i];
|
|
91
|
-
if (
|
|
112
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
92
117
|
i++;
|
|
93
118
|
}
|
|
94
119
|
if (i === 0) return NaN;
|
|
95
120
|
const num = Number(trimmed.slice(0, i));
|
|
96
121
|
if (Number.isNaN(num)) return NaN;
|
|
97
|
-
const unit = trimmed.slice(i)
|
|
122
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
123
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
124
|
+
if (divisor === void 0) return NaN;
|
|
125
|
+
if (unit === "") {
|
|
126
|
+
return num / divisor;
|
|
127
|
+
}
|
|
98
128
|
const mult = DURATION_UNITS[unit];
|
|
99
129
|
if (mult === void 0) return NaN;
|
|
100
130
|
const ms = num * mult;
|
|
101
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
102
|
-
if (divisor === void 0) return NaN;
|
|
103
131
|
return ms / divisor;
|
|
104
132
|
}
|
|
105
133
|
var BYTE_UNITS = {
|
|
@@ -130,6 +158,22 @@ var BYTE_UNITS = {
|
|
|
130
158
|
TiB: 1099511627776,
|
|
131
159
|
tebibyte: 1099511627776,
|
|
132
160
|
tebibytes: 1099511627776,
|
|
161
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
162
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
163
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
164
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
165
|
+
K: 1024,
|
|
166
|
+
k: 1024,
|
|
167
|
+
M: 1024 ** 2,
|
|
168
|
+
m: 1024 ** 2,
|
|
169
|
+
G: 1024 ** 3,
|
|
170
|
+
g: 1024 ** 3,
|
|
171
|
+
T: 1024 ** 4,
|
|
172
|
+
t: 1024 ** 4,
|
|
173
|
+
P: 1024 ** 5,
|
|
174
|
+
p: 1024 ** 5,
|
|
175
|
+
E: 1024 ** 6,
|
|
176
|
+
e: 1024 ** 6,
|
|
133
177
|
// lowercase short-form aliases
|
|
134
178
|
b: 1,
|
|
135
179
|
kb: 1e3,
|
|
@@ -153,25 +197,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
153
197
|
TiB: 1099511627776
|
|
154
198
|
};
|
|
155
199
|
function parseBytes(value, outputUnit = "B") {
|
|
156
|
-
const trimmed = value
|
|
200
|
+
const trimmed = trimHoconWs(value);
|
|
157
201
|
let i = 0;
|
|
158
202
|
while (i < trimmed.length) {
|
|
159
203
|
const ch = trimmed[i];
|
|
160
|
-
if (
|
|
204
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
161
209
|
i++;
|
|
162
210
|
}
|
|
163
211
|
if (i === 0) return NaN;
|
|
164
212
|
const num = Number(trimmed.slice(0, i));
|
|
165
213
|
if (Number.isNaN(num)) return NaN;
|
|
166
|
-
const unit = trimmed.slice(i)
|
|
214
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
215
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
216
|
+
if (divisor === void 0) return NaN;
|
|
217
|
+
if (unit === "") {
|
|
218
|
+
const bytes2 = Math.trunc(num);
|
|
219
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
220
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
221
|
+
}
|
|
222
|
+
return bytes2 / divisor;
|
|
223
|
+
}
|
|
167
224
|
let mult = BYTE_UNITS[unit];
|
|
168
225
|
if (mult === void 0) {
|
|
169
226
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
170
227
|
}
|
|
171
228
|
if (mult === void 0) return NaN;
|
|
172
229
|
const bytes = num * mult;
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
231
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
232
|
+
}
|
|
175
233
|
const result = bytes / divisor;
|
|
176
234
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
177
235
|
}
|
|
@@ -331,14 +389,6 @@ var Lexer = class {
|
|
|
331
389
|
continue;
|
|
332
390
|
}
|
|
333
391
|
if (isUnquotedStart(ch)) {
|
|
334
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
335
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
336
|
-
throw new ParseError(
|
|
337
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
338
|
-
sl,
|
|
339
|
-
sc
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
392
|
let value = "";
|
|
343
393
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
344
394
|
value += this.advance();
|
|
@@ -442,6 +492,10 @@ var Lexer = class {
|
|
|
442
492
|
/**
|
|
443
493
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
444
494
|
* Implements Appendix A state machine from the cross-language spec.
|
|
495
|
+
*
|
|
496
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
497
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
498
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
445
499
|
*/
|
|
446
500
|
parseSubstBody(startLine, startCol) {
|
|
447
501
|
let optional = false;
|
|
@@ -456,6 +510,7 @@ var Lexer = class {
|
|
|
456
510
|
let pendingWs = "";
|
|
457
511
|
const segments = [];
|
|
458
512
|
let lastDot = null;
|
|
513
|
+
let listSuffix = false;
|
|
459
514
|
while (true) {
|
|
460
515
|
if (this.pos >= this.input.length) {
|
|
461
516
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -465,6 +520,23 @@ var Lexer = class {
|
|
|
465
520
|
this.advance();
|
|
466
521
|
pendingWs = "";
|
|
467
522
|
break;
|
|
523
|
+
} else if (ch === "[") {
|
|
524
|
+
if (!curStarted) {
|
|
525
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
526
|
+
}
|
|
527
|
+
for (const w of pendingWs) {
|
|
528
|
+
if (w !== " " && w !== " ") {
|
|
529
|
+
throw new ParseError(
|
|
530
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
531
|
+
startLine,
|
|
532
|
+
this.col
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
537
|
+
pendingWs = "";
|
|
538
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
539
|
+
break;
|
|
468
540
|
} else if (ch === '"') {
|
|
469
541
|
const qLine = startLine;
|
|
470
542
|
const qCol = this.col;
|
|
@@ -524,15 +596,47 @@ var Lexer = class {
|
|
|
524
596
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
525
597
|
}
|
|
526
598
|
}
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
599
|
+
if (!listSuffix) {
|
|
600
|
+
if (curStarted) {
|
|
601
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
602
|
+
} else if (segments.length === 0) {
|
|
603
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
604
|
+
} else {
|
|
605
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
606
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
607
|
+
}
|
|
534
608
|
}
|
|
535
|
-
return { segments, optional };
|
|
609
|
+
return { segments, optional, listSuffix };
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
613
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
614
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
615
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
616
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
617
|
+
*/
|
|
618
|
+
parseListSuffix(startLine) {
|
|
619
|
+
this.advance();
|
|
620
|
+
const afterBracket = this.peek();
|
|
621
|
+
if (afterBracket !== "]") {
|
|
622
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
623
|
+
throw new ParseError(
|
|
624
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
625
|
+
startLine,
|
|
626
|
+
this.col
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
this.advance();
|
|
630
|
+
if (this.peek() !== "}") {
|
|
631
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
632
|
+
throw new ParseError(
|
|
633
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
634
|
+
startLine,
|
|
635
|
+
this.col
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
this.advance();
|
|
639
|
+
return true;
|
|
536
640
|
}
|
|
537
641
|
};
|
|
538
642
|
function tokenize(input) {
|
|
@@ -620,6 +724,14 @@ var Parser = class {
|
|
|
620
724
|
const t = this.peek();
|
|
621
725
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
622
726
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
727
|
+
const next = this.peek(1);
|
|
728
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
729
|
+
throw new ParseError(
|
|
730
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
731
|
+
t.line,
|
|
732
|
+
t.col
|
|
733
|
+
);
|
|
734
|
+
}
|
|
623
735
|
this.advance();
|
|
624
736
|
fields.push(this.parseInclude());
|
|
625
737
|
this.skip("newline");
|
|
@@ -628,7 +740,14 @@ var Parser = class {
|
|
|
628
740
|
continue;
|
|
629
741
|
}
|
|
630
742
|
const keyPos = this.currentPos();
|
|
631
|
-
const key = this.parseKey();
|
|
743
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
744
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
745
|
+
throw new ParseError(
|
|
746
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
747
|
+
keyPos.line,
|
|
748
|
+
keyPos.col
|
|
749
|
+
);
|
|
750
|
+
}
|
|
632
751
|
this.skip("newline");
|
|
633
752
|
let append = false;
|
|
634
753
|
const sep = this.peek();
|
|
@@ -658,10 +777,12 @@ var Parser = class {
|
|
|
658
777
|
parseKey() {
|
|
659
778
|
const segments = [];
|
|
660
779
|
let trailingDot = false;
|
|
780
|
+
let firstWasQuoted = false;
|
|
661
781
|
while (true) {
|
|
662
782
|
const t = this.peek();
|
|
663
783
|
if (t.kind === "string") {
|
|
664
784
|
this.advance();
|
|
785
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
665
786
|
segments.push(t.value);
|
|
666
787
|
trailingDot = false;
|
|
667
788
|
} else if (t.kind === "unquoted") {
|
|
@@ -698,7 +819,7 @@ var Parser = class {
|
|
|
698
819
|
}
|
|
699
820
|
break;
|
|
700
821
|
}
|
|
701
|
-
return segments;
|
|
822
|
+
return { segments, firstWasQuoted };
|
|
702
823
|
}
|
|
703
824
|
parseInclude() {
|
|
704
825
|
const p = this.currentPos();
|
|
@@ -777,7 +898,7 @@ var Parser = class {
|
|
|
777
898
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
778
899
|
}
|
|
779
900
|
const payload = t.subst;
|
|
780
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
901
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
781
902
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
782
903
|
this.advance();
|
|
783
904
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -828,6 +949,20 @@ function parseTokens(tokens) {
|
|
|
828
949
|
return new Parser(tokens).parse();
|
|
829
950
|
}
|
|
830
951
|
|
|
952
|
+
// src/internal/parser/empty-check.ts
|
|
953
|
+
function hasContentTokens(tokens) {
|
|
954
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
955
|
+
}
|
|
956
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
957
|
+
if (!hasContentTokens(tokens)) {
|
|
958
|
+
throw new ParseError(
|
|
959
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
960
|
+
1,
|
|
961
|
+
1
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
831
966
|
// src/internal/resolver/types.ts
|
|
832
967
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
833
968
|
function isSubst(v) {
|
|
@@ -929,6 +1064,7 @@ var nodePath = __toESM(require("path"), 1);
|
|
|
929
1064
|
// src/internal/properties/properties.ts
|
|
930
1065
|
function parseProperties(input) {
|
|
931
1066
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1067
|
+
const pairs = [];
|
|
932
1068
|
for (const line of input.split("\n")) {
|
|
933
1069
|
const trimmed = line.trim();
|
|
934
1070
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -937,6 +1073,10 @@ function parseProperties(input) {
|
|
|
937
1073
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
938
1074
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
939
1075
|
if (key === "") continue;
|
|
1076
|
+
pairs.push([key, value]);
|
|
1077
|
+
}
|
|
1078
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1079
|
+
for (const [key, value] of pairs) {
|
|
940
1080
|
setNested(root, key.split("."), value);
|
|
941
1081
|
}
|
|
942
1082
|
return root;
|
|
@@ -960,6 +1100,7 @@ function setNested(obj, segments, value) {
|
|
|
960
1100
|
}
|
|
961
1101
|
const last = segments[segments.length - 1];
|
|
962
1102
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1103
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
963
1104
|
current[last] = value;
|
|
964
1105
|
}
|
|
965
1106
|
function propertiesToHoconValue(input) {
|
|
@@ -1073,7 +1214,9 @@ var IncludeLoader = class {
|
|
|
1073
1214
|
if (candidate.endsWith(".properties")) {
|
|
1074
1215
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1075
1216
|
}
|
|
1076
|
-
const
|
|
1217
|
+
const tokens = tokenize(content);
|
|
1218
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1219
|
+
const ast = parseTokens(tokens);
|
|
1077
1220
|
return this.onBuildResObj(ast, {
|
|
1078
1221
|
env,
|
|
1079
1222
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1097,7 +1240,9 @@ var IncludeLoader = class {
|
|
|
1097
1240
|
if (candidate.endsWith(".properties")) {
|
|
1098
1241
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1099
1242
|
}
|
|
1100
|
-
const
|
|
1243
|
+
const tokens = tokenize(content);
|
|
1244
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1245
|
+
const ast = parseTokens(tokens);
|
|
1101
1246
|
return this.onBuildResObjAsync(ast, {
|
|
1102
1247
|
env,
|
|
1103
1248
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1234,9 +1379,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1234
1379
|
return inner;
|
|
1235
1380
|
}
|
|
1236
1381
|
case "subst":
|
|
1237
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1382
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1238
1383
|
case "concat":
|
|
1239
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1384
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1240
1385
|
case "include":
|
|
1241
1386
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1242
1387
|
}
|
|
@@ -1258,13 +1403,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1258
1403
|
case "object":
|
|
1259
1404
|
return await this.buildAsync(ast, pathPrefix);
|
|
1260
1405
|
case "subst":
|
|
1261
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1406
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1262
1407
|
case "concat": {
|
|
1263
1408
|
const nodes = [];
|
|
1264
1409
|
for (const n of ast.nodes) {
|
|
1265
1410
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1266
1411
|
}
|
|
1267
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1412
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1268
1413
|
}
|
|
1269
1414
|
case "include":
|
|
1270
1415
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1334,6 +1479,19 @@ var SubstitutionResolver = class {
|
|
|
1334
1479
|
this.opts = opts;
|
|
1335
1480
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1336
1481
|
this.cache = /* @__PURE__ */ new Map();
|
|
1482
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1483
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1484
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1485
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1486
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1487
|
+
//
|
|
1488
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1489
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1490
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1491
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1492
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1493
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1494
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1337
1495
|
}
|
|
1338
1496
|
resolve() {
|
|
1339
1497
|
return this.resolveResObj(this.root);
|
|
@@ -1372,7 +1530,14 @@ var SubstitutionResolver = class {
|
|
|
1372
1530
|
}
|
|
1373
1531
|
resolveVal(v, scope) {
|
|
1374
1532
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1375
|
-
if (isConcat(v))
|
|
1533
|
+
if (isConcat(v)) {
|
|
1534
|
+
this.resolvingConcats.add(v);
|
|
1535
|
+
try {
|
|
1536
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1537
|
+
} finally {
|
|
1538
|
+
this.resolvingConcats.delete(v);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1376
1541
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1377
1542
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1378
1543
|
const hv = v;
|
|
@@ -1386,15 +1551,12 @@ var SubstitutionResolver = class {
|
|
|
1386
1551
|
}
|
|
1387
1552
|
return hv;
|
|
1388
1553
|
}
|
|
1389
|
-
segmentsEqual(a, b) {
|
|
1390
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1391
|
-
}
|
|
1392
1554
|
resolveSubst(s, scope) {
|
|
1393
|
-
const key = segmentsToKey(s.segments);
|
|
1555
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1394
1556
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1395
1557
|
if (this.resolving.has(key)) {
|
|
1396
1558
|
let prior;
|
|
1397
|
-
if (s.
|
|
1559
|
+
if (s.segments.length > 1) {
|
|
1398
1560
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1399
1561
|
const parentScope = lookupResObj(
|
|
1400
1562
|
this.root,
|
|
@@ -1426,29 +1588,31 @@ var SubstitutionResolver = class {
|
|
|
1426
1588
|
try {
|
|
1427
1589
|
const found = lookupPath(this.root, s.segments);
|
|
1428
1590
|
if (found !== void 0) {
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1442
|
-
} else {
|
|
1443
|
-
const rootSeg = s.segments[0]?.text ?? "";
|
|
1444
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1445
|
-
}
|
|
1446
|
-
if (prior !== void 0) {
|
|
1447
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1448
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1449
|
-
return result2;
|
|
1450
|
-
}
|
|
1591
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1592
|
+
let prior;
|
|
1593
|
+
if (s.segments.length > 1) {
|
|
1594
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1595
|
+
const parentScope = lookupResObj(
|
|
1596
|
+
this.root,
|
|
1597
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1598
|
+
);
|
|
1599
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1600
|
+
} else {
|
|
1601
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1602
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1451
1603
|
}
|
|
1604
|
+
if (prior !== void 0) {
|
|
1605
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1606
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1607
|
+
return result2;
|
|
1608
|
+
}
|
|
1609
|
+
if (s.optional) return void 0;
|
|
1610
|
+
throw new ResolveError(
|
|
1611
|
+
`could not resolve substitution: \${${key}}`,
|
|
1612
|
+
key,
|
|
1613
|
+
s.line,
|
|
1614
|
+
s.col
|
|
1615
|
+
);
|
|
1452
1616
|
}
|
|
1453
1617
|
let result = this.resolveVal(found, scope);
|
|
1454
1618
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1477,6 +1641,21 @@ var SubstitutionResolver = class {
|
|
|
1477
1641
|
if (result !== void 0) this.cache.set(key, result);
|
|
1478
1642
|
return result;
|
|
1479
1643
|
}
|
|
1644
|
+
if (s.listSuffix) {
|
|
1645
|
+
const result = this.resolveEnvList(s);
|
|
1646
|
+
if (result !== void 0) {
|
|
1647
|
+
this.cache.set(key, result);
|
|
1648
|
+
return result;
|
|
1649
|
+
}
|
|
1650
|
+
if (s.optional) return void 0;
|
|
1651
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1652
|
+
throw new ResolveError(
|
|
1653
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1654
|
+
key,
|
|
1655
|
+
s.line,
|
|
1656
|
+
s.col
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1480
1659
|
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1481
1660
|
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1482
1661
|
if (envVal !== void 0) {
|
|
@@ -1495,7 +1674,42 @@ var SubstitutionResolver = class {
|
|
|
1495
1674
|
this.resolving.delete(key);
|
|
1496
1675
|
}
|
|
1497
1676
|
}
|
|
1498
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1679
|
+
*
|
|
1680
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1681
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1682
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1683
|
+
*
|
|
1684
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1685
|
+
* none do (caller applies optional/required handling).
|
|
1686
|
+
*/
|
|
1687
|
+
resolveEnvList(s) {
|
|
1688
|
+
const bases = this.candidateBases(s);
|
|
1689
|
+
for (const base of bases) {
|
|
1690
|
+
const key0 = base + "_0";
|
|
1691
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1692
|
+
const items = [];
|
|
1693
|
+
for (let i = 0; ; i++) {
|
|
1694
|
+
const k = base + "_" + i;
|
|
1695
|
+
if (!(k in this.opts.env)) break;
|
|
1696
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1697
|
+
}
|
|
1698
|
+
return { kind: "array", items };
|
|
1699
|
+
}
|
|
1700
|
+
return void 0;
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1704
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1705
|
+
*/
|
|
1706
|
+
candidateBases(s) {
|
|
1707
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1708
|
+
if (s.prefixLen === 0) return [full];
|
|
1709
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1710
|
+
return [full, bare];
|
|
1711
|
+
}
|
|
1712
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1499
1713
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1500
1714
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1501
1715
|
if (resolved.length === 1) return resolved[0];
|
|
@@ -1513,27 +1727,45 @@ var SubstitutionResolver = class {
|
|
|
1513
1727
|
if (converted !== null) {
|
|
1514
1728
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1515
1729
|
}
|
|
1516
|
-
|
|
1730
|
+
throw new ResolveError(
|
|
1731
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1732
|
+
"",
|
|
1733
|
+
line,
|
|
1734
|
+
col
|
|
1735
|
+
);
|
|
1517
1736
|
}
|
|
1518
1737
|
if (left.kind === "object" && right.kind === "array") {
|
|
1519
1738
|
const converted = numericObjectToArray(left);
|
|
1520
1739
|
if (converted !== null) {
|
|
1521
1740
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1522
1741
|
}
|
|
1523
|
-
|
|
1742
|
+
throw new ResolveError(
|
|
1743
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1744
|
+
"",
|
|
1745
|
+
line,
|
|
1746
|
+
col
|
|
1747
|
+
);
|
|
1524
1748
|
}
|
|
1525
1749
|
if (left.kind === "array" && right.kind === "array") {
|
|
1526
1750
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1527
1751
|
}
|
|
1528
|
-
if (left.kind === "array") {
|
|
1529
|
-
|
|
1752
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1753
|
+
throw new ResolveError(
|
|
1754
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1755
|
+
"",
|
|
1756
|
+
line,
|
|
1757
|
+
col
|
|
1758
|
+
);
|
|
1530
1759
|
}
|
|
1531
|
-
if (right.kind === "
|
|
1532
|
-
|
|
1760
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1761
|
+
throw new ResolveError(
|
|
1762
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1763
|
+
"",
|
|
1764
|
+
line,
|
|
1765
|
+
col
|
|
1766
|
+
);
|
|
1533
1767
|
}
|
|
1534
|
-
|
|
1535
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1536
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
1768
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1537
1769
|
};
|
|
1538
1770
|
const [head, ...tail] = nonSep;
|
|
1539
1771
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1600,6 +1832,7 @@ var Config = class _Config {
|
|
|
1600
1832
|
}
|
|
1601
1833
|
const result = parseBytes(v.raw, unit);
|
|
1602
1834
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1835
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1603
1836
|
return result;
|
|
1604
1837
|
}
|
|
1605
1838
|
getConfig(path2) {
|
|
@@ -1739,6 +1972,7 @@ function defaultReadFileSync(filePath) {
|
|
|
1739
1972
|
}
|
|
1740
1973
|
function buildResolveContext(input, opts) {
|
|
1741
1974
|
const tokens = tokenize(input);
|
|
1975
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1742
1976
|
const ast = parseTokens(tokens);
|
|
1743
1977
|
const resolveOpts = {
|
|
1744
1978
|
env: getEnv(opts),
|