@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/index.cjs
CHANGED
|
@@ -43,6 +43,27 @@ module.exports = __toCommonJS(src_exports);
|
|
|
43
43
|
|
|
44
44
|
// src/coerce.ts
|
|
45
45
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
46
|
+
function isHoconWs(cp) {
|
|
47
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
48
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
49
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
50
|
+
if (cp === 5760) return true;
|
|
51
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
52
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
53
|
+
if (cp === 12288) return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function trimHoconWs(s) {
|
|
57
|
+
let start = 0;
|
|
58
|
+
let end = s.length;
|
|
59
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
60
|
+
start++;
|
|
61
|
+
}
|
|
62
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
63
|
+
end--;
|
|
64
|
+
}
|
|
65
|
+
return s.slice(start, end);
|
|
66
|
+
}
|
|
46
67
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
47
68
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
48
69
|
function coerceBoolean(value) {
|
|
@@ -88,22 +109,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
88
109
|
d: 864e5
|
|
89
110
|
};
|
|
90
111
|
function parseDuration(value, outputUnit = "ms") {
|
|
91
|
-
const trimmed = value
|
|
112
|
+
const trimmed = trimHoconWs(value);
|
|
92
113
|
let i = 0;
|
|
93
114
|
while (i < trimmed.length) {
|
|
94
115
|
const ch = trimmed[i];
|
|
95
|
-
if (
|
|
116
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
117
|
+
i++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
96
121
|
i++;
|
|
97
122
|
}
|
|
98
123
|
if (i === 0) return NaN;
|
|
99
124
|
const num = Number(trimmed.slice(0, i));
|
|
100
125
|
if (Number.isNaN(num)) return NaN;
|
|
101
|
-
const unit = trimmed.slice(i)
|
|
126
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
127
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
128
|
+
if (divisor === void 0) return NaN;
|
|
129
|
+
if (unit === "") {
|
|
130
|
+
return num / divisor;
|
|
131
|
+
}
|
|
102
132
|
const mult = DURATION_UNITS[unit];
|
|
103
133
|
if (mult === void 0) return NaN;
|
|
104
134
|
const ms = num * mult;
|
|
105
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
106
|
-
if (divisor === void 0) return NaN;
|
|
107
135
|
return ms / divisor;
|
|
108
136
|
}
|
|
109
137
|
var BYTE_UNITS = {
|
|
@@ -134,6 +162,22 @@ var BYTE_UNITS = {
|
|
|
134
162
|
TiB: 1099511627776,
|
|
135
163
|
tebibyte: 1099511627776,
|
|
136
164
|
tebibytes: 1099511627776,
|
|
165
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
166
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
167
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
168
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
169
|
+
K: 1024,
|
|
170
|
+
k: 1024,
|
|
171
|
+
M: 1024 ** 2,
|
|
172
|
+
m: 1024 ** 2,
|
|
173
|
+
G: 1024 ** 3,
|
|
174
|
+
g: 1024 ** 3,
|
|
175
|
+
T: 1024 ** 4,
|
|
176
|
+
t: 1024 ** 4,
|
|
177
|
+
P: 1024 ** 5,
|
|
178
|
+
p: 1024 ** 5,
|
|
179
|
+
E: 1024 ** 6,
|
|
180
|
+
e: 1024 ** 6,
|
|
137
181
|
// lowercase short-form aliases
|
|
138
182
|
b: 1,
|
|
139
183
|
kb: 1e3,
|
|
@@ -157,25 +201,39 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
157
201
|
TiB: 1099511627776
|
|
158
202
|
};
|
|
159
203
|
function parseBytes(value, outputUnit = "B") {
|
|
160
|
-
const trimmed = value
|
|
204
|
+
const trimmed = trimHoconWs(value);
|
|
161
205
|
let i = 0;
|
|
162
206
|
while (i < trimmed.length) {
|
|
163
207
|
const ch = trimmed[i];
|
|
164
|
-
if (
|
|
208
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
165
213
|
i++;
|
|
166
214
|
}
|
|
167
215
|
if (i === 0) return NaN;
|
|
168
216
|
const num = Number(trimmed.slice(0, i));
|
|
169
217
|
if (Number.isNaN(num)) return NaN;
|
|
170
|
-
const unit = trimmed.slice(i)
|
|
218
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
219
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
220
|
+
if (divisor === void 0) return NaN;
|
|
221
|
+
if (unit === "") {
|
|
222
|
+
const bytes2 = Math.trunc(num);
|
|
223
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
224
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
225
|
+
}
|
|
226
|
+
return bytes2 / divisor;
|
|
227
|
+
}
|
|
171
228
|
let mult = BYTE_UNITS[unit];
|
|
172
229
|
if (mult === void 0) {
|
|
173
230
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
174
231
|
}
|
|
175
232
|
if (mult === void 0) return NaN;
|
|
176
233
|
const bytes = num * mult;
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
235
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
236
|
+
}
|
|
179
237
|
const result = bytes / divisor;
|
|
180
238
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
181
239
|
}
|
|
@@ -267,6 +325,7 @@ var Config = class _Config {
|
|
|
267
325
|
}
|
|
268
326
|
const result = parseBytes(v.raw, unit);
|
|
269
327
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
328
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
270
329
|
return result;
|
|
271
330
|
}
|
|
272
331
|
getConfig(path2) {
|
|
@@ -520,14 +579,6 @@ var Lexer = class {
|
|
|
520
579
|
continue;
|
|
521
580
|
}
|
|
522
581
|
if (isUnquotedStart(ch)) {
|
|
523
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
524
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
525
|
-
throw new ParseError(
|
|
526
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
527
|
-
sl,
|
|
528
|
-
sc
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
582
|
let value = "";
|
|
532
583
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
533
584
|
value += this.advance();
|
|
@@ -631,6 +682,10 @@ var Lexer = class {
|
|
|
631
682
|
/**
|
|
632
683
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
633
684
|
* Implements Appendix A state machine from the cross-language spec.
|
|
685
|
+
*
|
|
686
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
687
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
688
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
634
689
|
*/
|
|
635
690
|
parseSubstBody(startLine, startCol) {
|
|
636
691
|
let optional = false;
|
|
@@ -645,6 +700,7 @@ var Lexer = class {
|
|
|
645
700
|
let pendingWs = "";
|
|
646
701
|
const segments = [];
|
|
647
702
|
let lastDot = null;
|
|
703
|
+
let listSuffix = false;
|
|
648
704
|
while (true) {
|
|
649
705
|
if (this.pos >= this.input.length) {
|
|
650
706
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -654,6 +710,23 @@ var Lexer = class {
|
|
|
654
710
|
this.advance();
|
|
655
711
|
pendingWs = "";
|
|
656
712
|
break;
|
|
713
|
+
} else if (ch === "[") {
|
|
714
|
+
if (!curStarted) {
|
|
715
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
716
|
+
}
|
|
717
|
+
for (const w of pendingWs) {
|
|
718
|
+
if (w !== " " && w !== " ") {
|
|
719
|
+
throw new ParseError(
|
|
720
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
721
|
+
startLine,
|
|
722
|
+
this.col
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
727
|
+
pendingWs = "";
|
|
728
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
729
|
+
break;
|
|
657
730
|
} else if (ch === '"') {
|
|
658
731
|
const qLine = startLine;
|
|
659
732
|
const qCol = this.col;
|
|
@@ -713,15 +786,47 @@ var Lexer = class {
|
|
|
713
786
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
714
787
|
}
|
|
715
788
|
}
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
789
|
+
if (!listSuffix) {
|
|
790
|
+
if (curStarted) {
|
|
791
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
792
|
+
} else if (segments.length === 0) {
|
|
793
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
794
|
+
} else {
|
|
795
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
796
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
797
|
+
}
|
|
723
798
|
}
|
|
724
|
-
return { segments, optional };
|
|
799
|
+
return { segments, optional, listSuffix };
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
803
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
804
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
805
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
806
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
807
|
+
*/
|
|
808
|
+
parseListSuffix(startLine) {
|
|
809
|
+
this.advance();
|
|
810
|
+
const afterBracket = this.peek();
|
|
811
|
+
if (afterBracket !== "]") {
|
|
812
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
813
|
+
throw new ParseError(
|
|
814
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
815
|
+
startLine,
|
|
816
|
+
this.col
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this.advance();
|
|
820
|
+
if (this.peek() !== "}") {
|
|
821
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
822
|
+
throw new ParseError(
|
|
823
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
824
|
+
startLine,
|
|
825
|
+
this.col
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
this.advance();
|
|
829
|
+
return true;
|
|
725
830
|
}
|
|
726
831
|
};
|
|
727
832
|
function tokenize(input) {
|
|
@@ -809,6 +914,14 @@ var Parser = class {
|
|
|
809
914
|
const t = this.peek();
|
|
810
915
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
811
916
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
917
|
+
const next = this.peek(1);
|
|
918
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
919
|
+
throw new ParseError(
|
|
920
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
921
|
+
t.line,
|
|
922
|
+
t.col
|
|
923
|
+
);
|
|
924
|
+
}
|
|
812
925
|
this.advance();
|
|
813
926
|
fields.push(this.parseInclude());
|
|
814
927
|
this.skip("newline");
|
|
@@ -817,7 +930,14 @@ var Parser = class {
|
|
|
817
930
|
continue;
|
|
818
931
|
}
|
|
819
932
|
const keyPos = this.currentPos();
|
|
820
|
-
const key = this.parseKey();
|
|
933
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
934
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
935
|
+
throw new ParseError(
|
|
936
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
937
|
+
keyPos.line,
|
|
938
|
+
keyPos.col
|
|
939
|
+
);
|
|
940
|
+
}
|
|
821
941
|
this.skip("newline");
|
|
822
942
|
let append = false;
|
|
823
943
|
const sep = this.peek();
|
|
@@ -847,10 +967,12 @@ var Parser = class {
|
|
|
847
967
|
parseKey() {
|
|
848
968
|
const segments = [];
|
|
849
969
|
let trailingDot = false;
|
|
970
|
+
let firstWasQuoted = false;
|
|
850
971
|
while (true) {
|
|
851
972
|
const t = this.peek();
|
|
852
973
|
if (t.kind === "string") {
|
|
853
974
|
this.advance();
|
|
975
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
854
976
|
segments.push(t.value);
|
|
855
977
|
trailingDot = false;
|
|
856
978
|
} else if (t.kind === "unquoted") {
|
|
@@ -887,7 +1009,7 @@ var Parser = class {
|
|
|
887
1009
|
}
|
|
888
1010
|
break;
|
|
889
1011
|
}
|
|
890
|
-
return segments;
|
|
1012
|
+
return { segments, firstWasQuoted };
|
|
891
1013
|
}
|
|
892
1014
|
parseInclude() {
|
|
893
1015
|
const p = this.currentPos();
|
|
@@ -966,7 +1088,7 @@ var Parser = class {
|
|
|
966
1088
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
967
1089
|
}
|
|
968
1090
|
const payload = t.subst;
|
|
969
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
1091
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
970
1092
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
971
1093
|
this.advance();
|
|
972
1094
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -1017,6 +1139,20 @@ function parseTokens(tokens) {
|
|
|
1017
1139
|
return new Parser(tokens).parse();
|
|
1018
1140
|
}
|
|
1019
1141
|
|
|
1142
|
+
// src/internal/parser/empty-check.ts
|
|
1143
|
+
function hasContentTokens(tokens) {
|
|
1144
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
1145
|
+
}
|
|
1146
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
1147
|
+
if (!hasContentTokens(tokens)) {
|
|
1148
|
+
throw new ParseError(
|
|
1149
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
1150
|
+
1,
|
|
1151
|
+
1
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1020
1156
|
// src/internal/resolver/types.ts
|
|
1021
1157
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
1022
1158
|
function isSubst(v) {
|
|
@@ -1118,6 +1254,7 @@ var nodePath = __toESM(require("path"), 1);
|
|
|
1118
1254
|
// src/internal/properties/properties.ts
|
|
1119
1255
|
function parseProperties(input) {
|
|
1120
1256
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1257
|
+
const pairs = [];
|
|
1121
1258
|
for (const line of input.split("\n")) {
|
|
1122
1259
|
const trimmed = line.trim();
|
|
1123
1260
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -1126,6 +1263,10 @@ function parseProperties(input) {
|
|
|
1126
1263
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
1127
1264
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
1128
1265
|
if (key === "") continue;
|
|
1266
|
+
pairs.push([key, value]);
|
|
1267
|
+
}
|
|
1268
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1269
|
+
for (const [key, value] of pairs) {
|
|
1129
1270
|
setNested(root, key.split("."), value);
|
|
1130
1271
|
}
|
|
1131
1272
|
return root;
|
|
@@ -1149,6 +1290,7 @@ function setNested(obj, segments, value) {
|
|
|
1149
1290
|
}
|
|
1150
1291
|
const last = segments[segments.length - 1];
|
|
1151
1292
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1293
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
1152
1294
|
current[last] = value;
|
|
1153
1295
|
}
|
|
1154
1296
|
function propertiesToHoconValue(input) {
|
|
@@ -1262,7 +1404,9 @@ var IncludeLoader = class {
|
|
|
1262
1404
|
if (candidate.endsWith(".properties")) {
|
|
1263
1405
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1264
1406
|
}
|
|
1265
|
-
const
|
|
1407
|
+
const tokens = tokenize(content);
|
|
1408
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1409
|
+
const ast = parseTokens(tokens);
|
|
1266
1410
|
return this.onBuildResObj(ast, {
|
|
1267
1411
|
env,
|
|
1268
1412
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1286,7 +1430,9 @@ var IncludeLoader = class {
|
|
|
1286
1430
|
if (candidate.endsWith(".properties")) {
|
|
1287
1431
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1288
1432
|
}
|
|
1289
|
-
const
|
|
1433
|
+
const tokens = tokenize(content);
|
|
1434
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1435
|
+
const ast = parseTokens(tokens);
|
|
1290
1436
|
return this.onBuildResObjAsync(ast, {
|
|
1291
1437
|
env,
|
|
1292
1438
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1423,9 +1569,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1423
1569
|
return inner;
|
|
1424
1570
|
}
|
|
1425
1571
|
case "subst":
|
|
1426
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1572
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1427
1573
|
case "concat":
|
|
1428
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1574
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1429
1575
|
case "include":
|
|
1430
1576
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1431
1577
|
}
|
|
@@ -1447,13 +1593,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1447
1593
|
case "object":
|
|
1448
1594
|
return await this.buildAsync(ast, pathPrefix);
|
|
1449
1595
|
case "subst":
|
|
1450
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1596
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1451
1597
|
case "concat": {
|
|
1452
1598
|
const nodes = [];
|
|
1453
1599
|
for (const n of ast.nodes) {
|
|
1454
1600
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1455
1601
|
}
|
|
1456
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1602
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1457
1603
|
}
|
|
1458
1604
|
case "include":
|
|
1459
1605
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1506,6 +1652,19 @@ var SubstitutionResolver = class {
|
|
|
1506
1652
|
this.opts = opts;
|
|
1507
1653
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1508
1654
|
this.cache = /* @__PURE__ */ new Map();
|
|
1655
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1656
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1657
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1658
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1659
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1660
|
+
//
|
|
1661
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1662
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1663
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1664
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1665
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1666
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1667
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1509
1668
|
}
|
|
1510
1669
|
resolve() {
|
|
1511
1670
|
return this.resolveResObj(this.root);
|
|
@@ -1544,7 +1703,14 @@ var SubstitutionResolver = class {
|
|
|
1544
1703
|
}
|
|
1545
1704
|
resolveVal(v, scope) {
|
|
1546
1705
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1547
|
-
if (isConcat(v))
|
|
1706
|
+
if (isConcat(v)) {
|
|
1707
|
+
this.resolvingConcats.add(v);
|
|
1708
|
+
try {
|
|
1709
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1710
|
+
} finally {
|
|
1711
|
+
this.resolvingConcats.delete(v);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1548
1714
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1549
1715
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1550
1716
|
const hv = v;
|
|
@@ -1558,15 +1724,12 @@ var SubstitutionResolver = class {
|
|
|
1558
1724
|
}
|
|
1559
1725
|
return hv;
|
|
1560
1726
|
}
|
|
1561
|
-
segmentsEqual(a, b) {
|
|
1562
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1563
|
-
}
|
|
1564
1727
|
resolveSubst(s, scope) {
|
|
1565
|
-
const key = segmentsToKey(s.segments);
|
|
1728
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1566
1729
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1567
1730
|
if (this.resolving.has(key)) {
|
|
1568
1731
|
let prior;
|
|
1569
|
-
if (s.
|
|
1732
|
+
if (s.segments.length > 1) {
|
|
1570
1733
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1571
1734
|
const parentScope = lookupResObj(
|
|
1572
1735
|
this.root,
|
|
@@ -1598,29 +1761,31 @@ var SubstitutionResolver = class {
|
|
|
1598
1761
|
try {
|
|
1599
1762
|
const found = lookupPath(this.root, s.segments);
|
|
1600
1763
|
if (found !== void 0) {
|
|
1601
|
-
if (
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1614
|
-
} else {
|
|
1615
|
-
const rootSeg = s.segments[0]?.text ?? "";
|
|
1616
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1617
|
-
}
|
|
1618
|
-
if (prior !== void 0) {
|
|
1619
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1620
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1621
|
-
return result2;
|
|
1622
|
-
}
|
|
1764
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1765
|
+
let prior;
|
|
1766
|
+
if (s.segments.length > 1) {
|
|
1767
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1768
|
+
const parentScope = lookupResObj(
|
|
1769
|
+
this.root,
|
|
1770
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1771
|
+
);
|
|
1772
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1773
|
+
} else {
|
|
1774
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1775
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1623
1776
|
}
|
|
1777
|
+
if (prior !== void 0) {
|
|
1778
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1779
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1780
|
+
return result2;
|
|
1781
|
+
}
|
|
1782
|
+
if (s.optional) return void 0;
|
|
1783
|
+
throw new ResolveError(
|
|
1784
|
+
`could not resolve substitution: \${${key}}`,
|
|
1785
|
+
key,
|
|
1786
|
+
s.line,
|
|
1787
|
+
s.col
|
|
1788
|
+
);
|
|
1624
1789
|
}
|
|
1625
1790
|
let result = this.resolveVal(found, scope);
|
|
1626
1791
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1649,6 +1814,21 @@ var SubstitutionResolver = class {
|
|
|
1649
1814
|
if (result !== void 0) this.cache.set(key, result);
|
|
1650
1815
|
return result;
|
|
1651
1816
|
}
|
|
1817
|
+
if (s.listSuffix) {
|
|
1818
|
+
const result = this.resolveEnvList(s);
|
|
1819
|
+
if (result !== void 0) {
|
|
1820
|
+
this.cache.set(key, result);
|
|
1821
|
+
return result;
|
|
1822
|
+
}
|
|
1823
|
+
if (s.optional) return void 0;
|
|
1824
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1825
|
+
throw new ResolveError(
|
|
1826
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1827
|
+
key,
|
|
1828
|
+
s.line,
|
|
1829
|
+
s.col
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1652
1832
|
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1653
1833
|
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1654
1834
|
if (envVal !== void 0) {
|
|
@@ -1667,7 +1847,42 @@ var SubstitutionResolver = class {
|
|
|
1667
1847
|
this.resolving.delete(key);
|
|
1668
1848
|
}
|
|
1669
1849
|
}
|
|
1670
|
-
|
|
1850
|
+
/**
|
|
1851
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1852
|
+
*
|
|
1853
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1854
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1855
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1856
|
+
*
|
|
1857
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1858
|
+
* none do (caller applies optional/required handling).
|
|
1859
|
+
*/
|
|
1860
|
+
resolveEnvList(s) {
|
|
1861
|
+
const bases = this.candidateBases(s);
|
|
1862
|
+
for (const base of bases) {
|
|
1863
|
+
const key0 = base + "_0";
|
|
1864
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1865
|
+
const items = [];
|
|
1866
|
+
for (let i = 0; ; i++) {
|
|
1867
|
+
const k = base + "_" + i;
|
|
1868
|
+
if (!(k in this.opts.env)) break;
|
|
1869
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1870
|
+
}
|
|
1871
|
+
return { kind: "array", items };
|
|
1872
|
+
}
|
|
1873
|
+
return void 0;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1877
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1878
|
+
*/
|
|
1879
|
+
candidateBases(s) {
|
|
1880
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1881
|
+
if (s.prefixLen === 0) return [full];
|
|
1882
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1883
|
+
return [full, bare];
|
|
1884
|
+
}
|
|
1885
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1671
1886
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1672
1887
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1673
1888
|
if (resolved.length === 1) return resolved[0];
|
|
@@ -1685,27 +1900,45 @@ var SubstitutionResolver = class {
|
|
|
1685
1900
|
if (converted !== null) {
|
|
1686
1901
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1687
1902
|
}
|
|
1688
|
-
|
|
1903
|
+
throw new ResolveError(
|
|
1904
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1905
|
+
"",
|
|
1906
|
+
line,
|
|
1907
|
+
col
|
|
1908
|
+
);
|
|
1689
1909
|
}
|
|
1690
1910
|
if (left.kind === "object" && right.kind === "array") {
|
|
1691
1911
|
const converted = numericObjectToArray(left);
|
|
1692
1912
|
if (converted !== null) {
|
|
1693
1913
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1694
1914
|
}
|
|
1695
|
-
|
|
1915
|
+
throw new ResolveError(
|
|
1916
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1917
|
+
"",
|
|
1918
|
+
line,
|
|
1919
|
+
col
|
|
1920
|
+
);
|
|
1696
1921
|
}
|
|
1697
1922
|
if (left.kind === "array" && right.kind === "array") {
|
|
1698
1923
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1699
1924
|
}
|
|
1700
|
-
if (left.kind === "array") {
|
|
1701
|
-
|
|
1925
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1926
|
+
throw new ResolveError(
|
|
1927
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1928
|
+
"",
|
|
1929
|
+
line,
|
|
1930
|
+
col
|
|
1931
|
+
);
|
|
1702
1932
|
}
|
|
1703
|
-
if (right.kind === "
|
|
1704
|
-
|
|
1933
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1934
|
+
throw new ResolveError(
|
|
1935
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1936
|
+
"",
|
|
1937
|
+
line,
|
|
1938
|
+
col
|
|
1939
|
+
);
|
|
1705
1940
|
}
|
|
1706
|
-
|
|
1707
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1708
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
1941
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1709
1942
|
};
|
|
1710
1943
|
const [head, ...tail] = nonSep;
|
|
1711
1944
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1750,6 +1983,7 @@ async function defaultReadFile(filePath) {
|
|
|
1750
1983
|
}
|
|
1751
1984
|
function buildResolveContext(input, opts) {
|
|
1752
1985
|
const tokens = tokenize(input);
|
|
1986
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1753
1987
|
const ast = parseTokens(tokens);
|
|
1754
1988
|
const resolveOpts = {
|
|
1755
1989
|
env: getEnv(opts),
|