@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.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// src/coerce.ts
|
|
2
2
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
function isHoconWs(cp) {
|
|
4
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
5
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
6
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
7
|
+
if (cp === 5760) return true;
|
|
8
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
9
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
10
|
+
if (cp === 12288) return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function trimHoconWs(s) {
|
|
14
|
+
let start = 0;
|
|
15
|
+
let end = s.length;
|
|
16
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
17
|
+
start++;
|
|
18
|
+
}
|
|
19
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
20
|
+
end--;
|
|
21
|
+
}
|
|
22
|
+
return s.slice(start, end);
|
|
23
|
+
}
|
|
3
24
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
4
25
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
5
26
|
function coerceBoolean(value) {
|
|
@@ -45,22 +66,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
45
66
|
d: 864e5
|
|
46
67
|
};
|
|
47
68
|
function parseDuration(value, outputUnit = "ms") {
|
|
48
|
-
const trimmed = value
|
|
69
|
+
const trimmed = trimHoconWs(value);
|
|
49
70
|
let i = 0;
|
|
50
71
|
while (i < trimmed.length) {
|
|
51
72
|
const ch = trimmed[i];
|
|
52
|
-
if (
|
|
73
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
53
78
|
i++;
|
|
54
79
|
}
|
|
55
80
|
if (i === 0) return NaN;
|
|
56
81
|
const num = Number(trimmed.slice(0, i));
|
|
57
82
|
if (Number.isNaN(num)) return NaN;
|
|
58
|
-
const unit = trimmed.slice(i)
|
|
83
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
84
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
85
|
+
if (divisor === void 0) return NaN;
|
|
86
|
+
if (unit === "") {
|
|
87
|
+
return num / divisor;
|
|
88
|
+
}
|
|
59
89
|
const mult = DURATION_UNITS[unit];
|
|
60
90
|
if (mult === void 0) return NaN;
|
|
61
91
|
const ms = num * mult;
|
|
62
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
63
|
-
if (divisor === void 0) return NaN;
|
|
64
92
|
return ms / divisor;
|
|
65
93
|
}
|
|
66
94
|
var BYTE_UNITS = {
|
|
@@ -91,6 +119,22 @@ var BYTE_UNITS = {
|
|
|
91
119
|
TiB: 1099511627776,
|
|
92
120
|
tebibyte: 1099511627776,
|
|
93
121
|
tebibytes: 1099511627776,
|
|
122
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
123
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
124
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
125
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
126
|
+
K: 1024,
|
|
127
|
+
k: 1024,
|
|
128
|
+
M: 1024 ** 2,
|
|
129
|
+
m: 1024 ** 2,
|
|
130
|
+
G: 1024 ** 3,
|
|
131
|
+
g: 1024 ** 3,
|
|
132
|
+
T: 1024 ** 4,
|
|
133
|
+
t: 1024 ** 4,
|
|
134
|
+
P: 1024 ** 5,
|
|
135
|
+
p: 1024 ** 5,
|
|
136
|
+
E: 1024 ** 6,
|
|
137
|
+
e: 1024 ** 6,
|
|
94
138
|
// lowercase short-form aliases
|
|
95
139
|
b: 1,
|
|
96
140
|
kb: 1e3,
|
|
@@ -114,25 +158,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
114
158
|
TiB: 1099511627776
|
|
115
159
|
};
|
|
116
160
|
function parseBytes(value, outputUnit = "B") {
|
|
117
|
-
const trimmed = value
|
|
161
|
+
const trimmed = trimHoconWs(value);
|
|
118
162
|
let i = 0;
|
|
119
163
|
while (i < trimmed.length) {
|
|
120
164
|
const ch = trimmed[i];
|
|
121
|
-
if (
|
|
165
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
122
170
|
i++;
|
|
123
171
|
}
|
|
124
172
|
if (i === 0) return NaN;
|
|
125
173
|
const num = Number(trimmed.slice(0, i));
|
|
126
174
|
if (Number.isNaN(num)) return NaN;
|
|
127
|
-
const unit = trimmed.slice(i)
|
|
175
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
176
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
177
|
+
if (divisor === void 0) return NaN;
|
|
178
|
+
if (unit === "") {
|
|
179
|
+
const bytes2 = Math.trunc(num);
|
|
180
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
181
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
182
|
+
}
|
|
183
|
+
return bytes2 / divisor;
|
|
184
|
+
}
|
|
128
185
|
let mult = BYTE_UNITS[unit];
|
|
129
186
|
if (mult === void 0) {
|
|
130
187
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
131
188
|
}
|
|
132
189
|
if (mult === void 0) return NaN;
|
|
133
190
|
const bytes = num * mult;
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
192
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
193
|
+
}
|
|
136
194
|
const result = bytes / divisor;
|
|
137
195
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
138
196
|
}
|
|
@@ -292,14 +350,6 @@ var Lexer = class {
|
|
|
292
350
|
continue;
|
|
293
351
|
}
|
|
294
352
|
if (isUnquotedStart(ch)) {
|
|
295
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
296
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
297
|
-
throw new ParseError(
|
|
298
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
299
|
-
sl,
|
|
300
|
-
sc
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
353
|
let value = "";
|
|
304
354
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
305
355
|
value += this.advance();
|
|
@@ -403,6 +453,10 @@ var Lexer = class {
|
|
|
403
453
|
/**
|
|
404
454
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
405
455
|
* Implements Appendix A state machine from the cross-language spec.
|
|
456
|
+
*
|
|
457
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
458
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
459
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
406
460
|
*/
|
|
407
461
|
parseSubstBody(startLine, startCol) {
|
|
408
462
|
let optional = false;
|
|
@@ -417,6 +471,7 @@ var Lexer = class {
|
|
|
417
471
|
let pendingWs = "";
|
|
418
472
|
const segments = [];
|
|
419
473
|
let lastDot = null;
|
|
474
|
+
let listSuffix = false;
|
|
420
475
|
while (true) {
|
|
421
476
|
if (this.pos >= this.input.length) {
|
|
422
477
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -426,6 +481,23 @@ var Lexer = class {
|
|
|
426
481
|
this.advance();
|
|
427
482
|
pendingWs = "";
|
|
428
483
|
break;
|
|
484
|
+
} else if (ch === "[") {
|
|
485
|
+
if (!curStarted) {
|
|
486
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
487
|
+
}
|
|
488
|
+
for (const w of pendingWs) {
|
|
489
|
+
if (w !== " " && w !== " ") {
|
|
490
|
+
throw new ParseError(
|
|
491
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
492
|
+
startLine,
|
|
493
|
+
this.col
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
498
|
+
pendingWs = "";
|
|
499
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
500
|
+
break;
|
|
429
501
|
} else if (ch === '"') {
|
|
430
502
|
const qLine = startLine;
|
|
431
503
|
const qCol = this.col;
|
|
@@ -485,15 +557,47 @@ var Lexer = class {
|
|
|
485
557
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
486
558
|
}
|
|
487
559
|
}
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
560
|
+
if (!listSuffix) {
|
|
561
|
+
if (curStarted) {
|
|
562
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
563
|
+
} else if (segments.length === 0) {
|
|
564
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
565
|
+
} else {
|
|
566
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
567
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
568
|
+
}
|
|
495
569
|
}
|
|
496
|
-
return { segments, optional };
|
|
570
|
+
return { segments, optional, listSuffix };
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
574
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
575
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
576
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
577
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
578
|
+
*/
|
|
579
|
+
parseListSuffix(startLine) {
|
|
580
|
+
this.advance();
|
|
581
|
+
const afterBracket = this.peek();
|
|
582
|
+
if (afterBracket !== "]") {
|
|
583
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
584
|
+
throw new ParseError(
|
|
585
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
586
|
+
startLine,
|
|
587
|
+
this.col
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
this.advance();
|
|
591
|
+
if (this.peek() !== "}") {
|
|
592
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
593
|
+
throw new ParseError(
|
|
594
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
595
|
+
startLine,
|
|
596
|
+
this.col
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
this.advance();
|
|
600
|
+
return true;
|
|
497
601
|
}
|
|
498
602
|
};
|
|
499
603
|
function tokenize(input) {
|
|
@@ -581,6 +685,14 @@ var Parser = class {
|
|
|
581
685
|
const t = this.peek();
|
|
582
686
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
583
687
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
688
|
+
const next = this.peek(1);
|
|
689
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
690
|
+
throw new ParseError(
|
|
691
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
692
|
+
t.line,
|
|
693
|
+
t.col
|
|
694
|
+
);
|
|
695
|
+
}
|
|
584
696
|
this.advance();
|
|
585
697
|
fields.push(this.parseInclude());
|
|
586
698
|
this.skip("newline");
|
|
@@ -589,7 +701,14 @@ var Parser = class {
|
|
|
589
701
|
continue;
|
|
590
702
|
}
|
|
591
703
|
const keyPos = this.currentPos();
|
|
592
|
-
const key = this.parseKey();
|
|
704
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
705
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
706
|
+
throw new ParseError(
|
|
707
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
708
|
+
keyPos.line,
|
|
709
|
+
keyPos.col
|
|
710
|
+
);
|
|
711
|
+
}
|
|
593
712
|
this.skip("newline");
|
|
594
713
|
let append = false;
|
|
595
714
|
const sep = this.peek();
|
|
@@ -619,10 +738,12 @@ var Parser = class {
|
|
|
619
738
|
parseKey() {
|
|
620
739
|
const segments = [];
|
|
621
740
|
let trailingDot = false;
|
|
741
|
+
let firstWasQuoted = false;
|
|
622
742
|
while (true) {
|
|
623
743
|
const t = this.peek();
|
|
624
744
|
if (t.kind === "string") {
|
|
625
745
|
this.advance();
|
|
746
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
626
747
|
segments.push(t.value);
|
|
627
748
|
trailingDot = false;
|
|
628
749
|
} else if (t.kind === "unquoted") {
|
|
@@ -659,7 +780,7 @@ var Parser = class {
|
|
|
659
780
|
}
|
|
660
781
|
break;
|
|
661
782
|
}
|
|
662
|
-
return segments;
|
|
783
|
+
return { segments, firstWasQuoted };
|
|
663
784
|
}
|
|
664
785
|
parseInclude() {
|
|
665
786
|
const p = this.currentPos();
|
|
@@ -738,7 +859,7 @@ var Parser = class {
|
|
|
738
859
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
739
860
|
}
|
|
740
861
|
const payload = t.subst;
|
|
741
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
862
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
742
863
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
743
864
|
this.advance();
|
|
744
865
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -789,6 +910,20 @@ function parseTokens(tokens) {
|
|
|
789
910
|
return new Parser(tokens).parse();
|
|
790
911
|
}
|
|
791
912
|
|
|
913
|
+
// src/internal/parser/empty-check.ts
|
|
914
|
+
function hasContentTokens(tokens) {
|
|
915
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
916
|
+
}
|
|
917
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
918
|
+
if (!hasContentTokens(tokens)) {
|
|
919
|
+
throw new ParseError(
|
|
920
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
921
|
+
1,
|
|
922
|
+
1
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
792
927
|
// src/internal/resolver/types.ts
|
|
793
928
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
794
929
|
function isSubst(v) {
|
|
@@ -890,6 +1025,7 @@ import * as nodePath from "path";
|
|
|
890
1025
|
// src/internal/properties/properties.ts
|
|
891
1026
|
function parseProperties(input) {
|
|
892
1027
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1028
|
+
const pairs = [];
|
|
893
1029
|
for (const line of input.split("\n")) {
|
|
894
1030
|
const trimmed = line.trim();
|
|
895
1031
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -898,6 +1034,10 @@ function parseProperties(input) {
|
|
|
898
1034
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
899
1035
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
900
1036
|
if (key === "") continue;
|
|
1037
|
+
pairs.push([key, value]);
|
|
1038
|
+
}
|
|
1039
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1040
|
+
for (const [key, value] of pairs) {
|
|
901
1041
|
setNested(root, key.split("."), value);
|
|
902
1042
|
}
|
|
903
1043
|
return root;
|
|
@@ -921,6 +1061,7 @@ function setNested(obj, segments, value) {
|
|
|
921
1061
|
}
|
|
922
1062
|
const last = segments[segments.length - 1];
|
|
923
1063
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1064
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
924
1065
|
current[last] = value;
|
|
925
1066
|
}
|
|
926
1067
|
function propertiesToHoconValue(input) {
|
|
@@ -1034,7 +1175,9 @@ var IncludeLoader = class {
|
|
|
1034
1175
|
if (candidate.endsWith(".properties")) {
|
|
1035
1176
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1036
1177
|
}
|
|
1037
|
-
const
|
|
1178
|
+
const tokens = tokenize(content);
|
|
1179
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1180
|
+
const ast = parseTokens(tokens);
|
|
1038
1181
|
return this.onBuildResObj(ast, {
|
|
1039
1182
|
env,
|
|
1040
1183
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1058,7 +1201,9 @@ var IncludeLoader = class {
|
|
|
1058
1201
|
if (candidate.endsWith(".properties")) {
|
|
1059
1202
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1060
1203
|
}
|
|
1061
|
-
const
|
|
1204
|
+
const tokens = tokenize(content);
|
|
1205
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1206
|
+
const ast = parseTokens(tokens);
|
|
1062
1207
|
return this.onBuildResObjAsync(ast, {
|
|
1063
1208
|
env,
|
|
1064
1209
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1195,9 +1340,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1195
1340
|
return inner;
|
|
1196
1341
|
}
|
|
1197
1342
|
case "subst":
|
|
1198
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1343
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1199
1344
|
case "concat":
|
|
1200
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1345
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1201
1346
|
case "include":
|
|
1202
1347
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1203
1348
|
}
|
|
@@ -1219,13 +1364,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1219
1364
|
case "object":
|
|
1220
1365
|
return await this.buildAsync(ast, pathPrefix);
|
|
1221
1366
|
case "subst":
|
|
1222
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1367
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1223
1368
|
case "concat": {
|
|
1224
1369
|
const nodes = [];
|
|
1225
1370
|
for (const n of ast.nodes) {
|
|
1226
1371
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1227
1372
|
}
|
|
1228
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1373
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1229
1374
|
}
|
|
1230
1375
|
case "include":
|
|
1231
1376
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1295,6 +1440,19 @@ var SubstitutionResolver = class {
|
|
|
1295
1440
|
this.opts = opts;
|
|
1296
1441
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1297
1442
|
this.cache = /* @__PURE__ */ new Map();
|
|
1443
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1444
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1445
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1446
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1447
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1448
|
+
//
|
|
1449
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1450
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1451
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1452
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1453
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1454
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1455
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1298
1456
|
}
|
|
1299
1457
|
resolve() {
|
|
1300
1458
|
return this.resolveResObj(this.root);
|
|
@@ -1333,7 +1491,14 @@ var SubstitutionResolver = class {
|
|
|
1333
1491
|
}
|
|
1334
1492
|
resolveVal(v, scope) {
|
|
1335
1493
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1336
|
-
if (isConcat(v))
|
|
1494
|
+
if (isConcat(v)) {
|
|
1495
|
+
this.resolvingConcats.add(v);
|
|
1496
|
+
try {
|
|
1497
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1498
|
+
} finally {
|
|
1499
|
+
this.resolvingConcats.delete(v);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1337
1502
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1338
1503
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1339
1504
|
const hv = v;
|
|
@@ -1347,15 +1512,12 @@ var SubstitutionResolver = class {
|
|
|
1347
1512
|
}
|
|
1348
1513
|
return hv;
|
|
1349
1514
|
}
|
|
1350
|
-
segmentsEqual(a, b) {
|
|
1351
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1352
|
-
}
|
|
1353
1515
|
resolveSubst(s, scope) {
|
|
1354
|
-
const key = segmentsToKey(s.segments);
|
|
1516
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1355
1517
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1356
1518
|
if (this.resolving.has(key)) {
|
|
1357
1519
|
let prior;
|
|
1358
|
-
if (s.
|
|
1520
|
+
if (s.segments.length > 1) {
|
|
1359
1521
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1360
1522
|
const parentScope = lookupResObj(
|
|
1361
1523
|
this.root,
|
|
@@ -1387,29 +1549,31 @@ var SubstitutionResolver = class {
|
|
|
1387
1549
|
try {
|
|
1388
1550
|
const found = lookupPath(this.root, s.segments);
|
|
1389
1551
|
if (found !== void 0) {
|
|
1390
|
-
if (
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1403
|
-
} else {
|
|
1404
|
-
const rootSeg = s.segments[0]?.text ?? "";
|
|
1405
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1406
|
-
}
|
|
1407
|
-
if (prior !== void 0) {
|
|
1408
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1409
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1410
|
-
return result2;
|
|
1411
|
-
}
|
|
1552
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1553
|
+
let prior;
|
|
1554
|
+
if (s.segments.length > 1) {
|
|
1555
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1556
|
+
const parentScope = lookupResObj(
|
|
1557
|
+
this.root,
|
|
1558
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1559
|
+
);
|
|
1560
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1561
|
+
} else {
|
|
1562
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1563
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1412
1564
|
}
|
|
1565
|
+
if (prior !== void 0) {
|
|
1566
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1567
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1568
|
+
return result2;
|
|
1569
|
+
}
|
|
1570
|
+
if (s.optional) return void 0;
|
|
1571
|
+
throw new ResolveError(
|
|
1572
|
+
`could not resolve substitution: \${${key}}`,
|
|
1573
|
+
key,
|
|
1574
|
+
s.line,
|
|
1575
|
+
s.col
|
|
1576
|
+
);
|
|
1413
1577
|
}
|
|
1414
1578
|
let result = this.resolveVal(found, scope);
|
|
1415
1579
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1438,6 +1602,21 @@ var SubstitutionResolver = class {
|
|
|
1438
1602
|
if (result !== void 0) this.cache.set(key, result);
|
|
1439
1603
|
return result;
|
|
1440
1604
|
}
|
|
1605
|
+
if (s.listSuffix) {
|
|
1606
|
+
const result = this.resolveEnvList(s);
|
|
1607
|
+
if (result !== void 0) {
|
|
1608
|
+
this.cache.set(key, result);
|
|
1609
|
+
return result;
|
|
1610
|
+
}
|
|
1611
|
+
if (s.optional) return void 0;
|
|
1612
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1613
|
+
throw new ResolveError(
|
|
1614
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1615
|
+
key,
|
|
1616
|
+
s.line,
|
|
1617
|
+
s.col
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1441
1620
|
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1442
1621
|
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1443
1622
|
if (envVal !== void 0) {
|
|
@@ -1456,7 +1635,42 @@ var SubstitutionResolver = class {
|
|
|
1456
1635
|
this.resolving.delete(key);
|
|
1457
1636
|
}
|
|
1458
1637
|
}
|
|
1459
|
-
|
|
1638
|
+
/**
|
|
1639
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1640
|
+
*
|
|
1641
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1642
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1643
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1644
|
+
*
|
|
1645
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1646
|
+
* none do (caller applies optional/required handling).
|
|
1647
|
+
*/
|
|
1648
|
+
resolveEnvList(s) {
|
|
1649
|
+
const bases = this.candidateBases(s);
|
|
1650
|
+
for (const base of bases) {
|
|
1651
|
+
const key0 = base + "_0";
|
|
1652
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1653
|
+
const items = [];
|
|
1654
|
+
for (let i = 0; ; i++) {
|
|
1655
|
+
const k = base + "_" + i;
|
|
1656
|
+
if (!(k in this.opts.env)) break;
|
|
1657
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1658
|
+
}
|
|
1659
|
+
return { kind: "array", items };
|
|
1660
|
+
}
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1665
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1666
|
+
*/
|
|
1667
|
+
candidateBases(s) {
|
|
1668
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1669
|
+
if (s.prefixLen === 0) return [full];
|
|
1670
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1671
|
+
return [full, bare];
|
|
1672
|
+
}
|
|
1673
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1460
1674
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1461
1675
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1462
1676
|
if (resolved.length === 1) return resolved[0];
|
|
@@ -1474,27 +1688,45 @@ var SubstitutionResolver = class {
|
|
|
1474
1688
|
if (converted !== null) {
|
|
1475
1689
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1476
1690
|
}
|
|
1477
|
-
|
|
1691
|
+
throw new ResolveError(
|
|
1692
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1693
|
+
"",
|
|
1694
|
+
line,
|
|
1695
|
+
col
|
|
1696
|
+
);
|
|
1478
1697
|
}
|
|
1479
1698
|
if (left.kind === "object" && right.kind === "array") {
|
|
1480
1699
|
const converted = numericObjectToArray(left);
|
|
1481
1700
|
if (converted !== null) {
|
|
1482
1701
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1483
1702
|
}
|
|
1484
|
-
|
|
1703
|
+
throw new ResolveError(
|
|
1704
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1705
|
+
"",
|
|
1706
|
+
line,
|
|
1707
|
+
col
|
|
1708
|
+
);
|
|
1485
1709
|
}
|
|
1486
1710
|
if (left.kind === "array" && right.kind === "array") {
|
|
1487
1711
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1488
1712
|
}
|
|
1489
|
-
if (left.kind === "array") {
|
|
1490
|
-
|
|
1713
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1714
|
+
throw new ResolveError(
|
|
1715
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1716
|
+
"",
|
|
1717
|
+
line,
|
|
1718
|
+
col
|
|
1719
|
+
);
|
|
1491
1720
|
}
|
|
1492
|
-
if (right.kind === "
|
|
1493
|
-
|
|
1721
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1722
|
+
throw new ResolveError(
|
|
1723
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1724
|
+
"",
|
|
1725
|
+
line,
|
|
1726
|
+
col
|
|
1727
|
+
);
|
|
1494
1728
|
}
|
|
1495
|
-
|
|
1496
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1497
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
1729
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1498
1730
|
};
|
|
1499
1731
|
const [head, ...tail] = nonSep;
|
|
1500
1732
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1561,6 +1793,7 @@ var Config = class _Config {
|
|
|
1561
1793
|
}
|
|
1562
1794
|
const result = parseBytes(v.raw, unit);
|
|
1563
1795
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1796
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1564
1797
|
return result;
|
|
1565
1798
|
}
|
|
1566
1799
|
getConfig(path2) {
|
|
@@ -1700,6 +1933,7 @@ function defaultReadFileSync(filePath) {
|
|
|
1700
1933
|
}
|
|
1701
1934
|
function buildResolveContext(input, opts) {
|
|
1702
1935
|
const tokens = tokenize(input);
|
|
1936
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1703
1937
|
const ast = parseTokens(tokens);
|
|
1704
1938
|
const resolveOpts = {
|
|
1705
1939
|
env: getEnv(opts),
|