@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.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
|
}
|
|
@@ -224,6 +282,7 @@ var Config = class _Config {
|
|
|
224
282
|
}
|
|
225
283
|
const result = parseBytes(v.raw, unit);
|
|
226
284
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
285
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
227
286
|
return result;
|
|
228
287
|
}
|
|
229
288
|
getConfig(path2) {
|
|
@@ -477,14 +536,6 @@ var Lexer = class {
|
|
|
477
536
|
continue;
|
|
478
537
|
}
|
|
479
538
|
if (isUnquotedStart(ch)) {
|
|
480
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
481
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
482
|
-
throw new ParseError(
|
|
483
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
484
|
-
sl,
|
|
485
|
-
sc
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
539
|
let value = "";
|
|
489
540
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
490
541
|
value += this.advance();
|
|
@@ -588,6 +639,10 @@ var Lexer = class {
|
|
|
588
639
|
/**
|
|
589
640
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
590
641
|
* Implements Appendix A state machine from the cross-language spec.
|
|
642
|
+
*
|
|
643
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
644
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
645
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
591
646
|
*/
|
|
592
647
|
parseSubstBody(startLine, startCol) {
|
|
593
648
|
let optional = false;
|
|
@@ -602,6 +657,7 @@ var Lexer = class {
|
|
|
602
657
|
let pendingWs = "";
|
|
603
658
|
const segments = [];
|
|
604
659
|
let lastDot = null;
|
|
660
|
+
let listSuffix = false;
|
|
605
661
|
while (true) {
|
|
606
662
|
if (this.pos >= this.input.length) {
|
|
607
663
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -611,6 +667,23 @@ var Lexer = class {
|
|
|
611
667
|
this.advance();
|
|
612
668
|
pendingWs = "";
|
|
613
669
|
break;
|
|
670
|
+
} else if (ch === "[") {
|
|
671
|
+
if (!curStarted) {
|
|
672
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
673
|
+
}
|
|
674
|
+
for (const w of pendingWs) {
|
|
675
|
+
if (w !== " " && w !== " ") {
|
|
676
|
+
throw new ParseError(
|
|
677
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
678
|
+
startLine,
|
|
679
|
+
this.col
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
684
|
+
pendingWs = "";
|
|
685
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
686
|
+
break;
|
|
614
687
|
} else if (ch === '"') {
|
|
615
688
|
const qLine = startLine;
|
|
616
689
|
const qCol = this.col;
|
|
@@ -670,15 +743,47 @@ var Lexer = class {
|
|
|
670
743
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
671
744
|
}
|
|
672
745
|
}
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
746
|
+
if (!listSuffix) {
|
|
747
|
+
if (curStarted) {
|
|
748
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
749
|
+
} else if (segments.length === 0) {
|
|
750
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
751
|
+
} else {
|
|
752
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
753
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
754
|
+
}
|
|
680
755
|
}
|
|
681
|
-
return { segments, optional };
|
|
756
|
+
return { segments, optional, listSuffix };
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
760
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
761
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
762
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
763
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
764
|
+
*/
|
|
765
|
+
parseListSuffix(startLine) {
|
|
766
|
+
this.advance();
|
|
767
|
+
const afterBracket = this.peek();
|
|
768
|
+
if (afterBracket !== "]") {
|
|
769
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
770
|
+
throw new ParseError(
|
|
771
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
772
|
+
startLine,
|
|
773
|
+
this.col
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
this.advance();
|
|
777
|
+
if (this.peek() !== "}") {
|
|
778
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
779
|
+
throw new ParseError(
|
|
780
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
781
|
+
startLine,
|
|
782
|
+
this.col
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
this.advance();
|
|
786
|
+
return true;
|
|
682
787
|
}
|
|
683
788
|
};
|
|
684
789
|
function tokenize(input) {
|
|
@@ -766,6 +871,14 @@ var Parser = class {
|
|
|
766
871
|
const t = this.peek();
|
|
767
872
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
768
873
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
874
|
+
const next = this.peek(1);
|
|
875
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
876
|
+
throw new ParseError(
|
|
877
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
878
|
+
t.line,
|
|
879
|
+
t.col
|
|
880
|
+
);
|
|
881
|
+
}
|
|
769
882
|
this.advance();
|
|
770
883
|
fields.push(this.parseInclude());
|
|
771
884
|
this.skip("newline");
|
|
@@ -774,7 +887,14 @@ var Parser = class {
|
|
|
774
887
|
continue;
|
|
775
888
|
}
|
|
776
889
|
const keyPos = this.currentPos();
|
|
777
|
-
const key = this.parseKey();
|
|
890
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
891
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
892
|
+
throw new ParseError(
|
|
893
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
894
|
+
keyPos.line,
|
|
895
|
+
keyPos.col
|
|
896
|
+
);
|
|
897
|
+
}
|
|
778
898
|
this.skip("newline");
|
|
779
899
|
let append = false;
|
|
780
900
|
const sep = this.peek();
|
|
@@ -804,10 +924,12 @@ var Parser = class {
|
|
|
804
924
|
parseKey() {
|
|
805
925
|
const segments = [];
|
|
806
926
|
let trailingDot = false;
|
|
927
|
+
let firstWasQuoted = false;
|
|
807
928
|
while (true) {
|
|
808
929
|
const t = this.peek();
|
|
809
930
|
if (t.kind === "string") {
|
|
810
931
|
this.advance();
|
|
932
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
811
933
|
segments.push(t.value);
|
|
812
934
|
trailingDot = false;
|
|
813
935
|
} else if (t.kind === "unquoted") {
|
|
@@ -844,7 +966,7 @@ var Parser = class {
|
|
|
844
966
|
}
|
|
845
967
|
break;
|
|
846
968
|
}
|
|
847
|
-
return segments;
|
|
969
|
+
return { segments, firstWasQuoted };
|
|
848
970
|
}
|
|
849
971
|
parseInclude() {
|
|
850
972
|
const p = this.currentPos();
|
|
@@ -923,7 +1045,7 @@ var Parser = class {
|
|
|
923
1045
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
924
1046
|
}
|
|
925
1047
|
const payload = t.subst;
|
|
926
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
1048
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
927
1049
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
928
1050
|
this.advance();
|
|
929
1051
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -974,6 +1096,20 @@ function parseTokens(tokens) {
|
|
|
974
1096
|
return new Parser(tokens).parse();
|
|
975
1097
|
}
|
|
976
1098
|
|
|
1099
|
+
// src/internal/parser/empty-check.ts
|
|
1100
|
+
function hasContentTokens(tokens) {
|
|
1101
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
1102
|
+
}
|
|
1103
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
1104
|
+
if (!hasContentTokens(tokens)) {
|
|
1105
|
+
throw new ParseError(
|
|
1106
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
1107
|
+
1,
|
|
1108
|
+
1
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
977
1113
|
// src/internal/resolver/types.ts
|
|
978
1114
|
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
979
1115
|
function isSubst(v) {
|
|
@@ -1075,6 +1211,7 @@ import * as nodePath from "path";
|
|
|
1075
1211
|
// src/internal/properties/properties.ts
|
|
1076
1212
|
function parseProperties(input) {
|
|
1077
1213
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1214
|
+
const pairs = [];
|
|
1078
1215
|
for (const line of input.split("\n")) {
|
|
1079
1216
|
const trimmed = line.trim();
|
|
1080
1217
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -1083,6 +1220,10 @@ function parseProperties(input) {
|
|
|
1083
1220
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
1084
1221
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
1085
1222
|
if (key === "") continue;
|
|
1223
|
+
pairs.push([key, value]);
|
|
1224
|
+
}
|
|
1225
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1226
|
+
for (const [key, value] of pairs) {
|
|
1086
1227
|
setNested(root, key.split("."), value);
|
|
1087
1228
|
}
|
|
1088
1229
|
return root;
|
|
@@ -1106,6 +1247,7 @@ function setNested(obj, segments, value) {
|
|
|
1106
1247
|
}
|
|
1107
1248
|
const last = segments[segments.length - 1];
|
|
1108
1249
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1250
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
1109
1251
|
current[last] = value;
|
|
1110
1252
|
}
|
|
1111
1253
|
function propertiesToHoconValue(input) {
|
|
@@ -1219,7 +1361,9 @@ var IncludeLoader = class {
|
|
|
1219
1361
|
if (candidate.endsWith(".properties")) {
|
|
1220
1362
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1221
1363
|
}
|
|
1222
|
-
const
|
|
1364
|
+
const tokens = tokenize(content);
|
|
1365
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1366
|
+
const ast = parseTokens(tokens);
|
|
1223
1367
|
return this.onBuildResObj(ast, {
|
|
1224
1368
|
env,
|
|
1225
1369
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1243,7 +1387,9 @@ var IncludeLoader = class {
|
|
|
1243
1387
|
if (candidate.endsWith(".properties")) {
|
|
1244
1388
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1245
1389
|
}
|
|
1246
|
-
const
|
|
1390
|
+
const tokens = tokenize(content);
|
|
1391
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1392
|
+
const ast = parseTokens(tokens);
|
|
1247
1393
|
return this.onBuildResObjAsync(ast, {
|
|
1248
1394
|
env,
|
|
1249
1395
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1380,9 +1526,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1380
1526
|
return inner;
|
|
1381
1527
|
}
|
|
1382
1528
|
case "subst":
|
|
1383
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1529
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1384
1530
|
case "concat":
|
|
1385
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1531
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1386
1532
|
case "include":
|
|
1387
1533
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1388
1534
|
}
|
|
@@ -1404,13 +1550,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1404
1550
|
case "object":
|
|
1405
1551
|
return await this.buildAsync(ast, pathPrefix);
|
|
1406
1552
|
case "subst":
|
|
1407
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1553
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1408
1554
|
case "concat": {
|
|
1409
1555
|
const nodes = [];
|
|
1410
1556
|
for (const n of ast.nodes) {
|
|
1411
1557
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1412
1558
|
}
|
|
1413
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1559
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1414
1560
|
}
|
|
1415
1561
|
case "include":
|
|
1416
1562
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1463,6 +1609,19 @@ var SubstitutionResolver = class {
|
|
|
1463
1609
|
this.opts = opts;
|
|
1464
1610
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1465
1611
|
this.cache = /* @__PURE__ */ new Map();
|
|
1612
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1613
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1614
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1615
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1616
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1617
|
+
//
|
|
1618
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1619
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1620
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1621
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1622
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1623
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1624
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1466
1625
|
}
|
|
1467
1626
|
resolve() {
|
|
1468
1627
|
return this.resolveResObj(this.root);
|
|
@@ -1501,7 +1660,14 @@ var SubstitutionResolver = class {
|
|
|
1501
1660
|
}
|
|
1502
1661
|
resolveVal(v, scope) {
|
|
1503
1662
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1504
|
-
if (isConcat(v))
|
|
1663
|
+
if (isConcat(v)) {
|
|
1664
|
+
this.resolvingConcats.add(v);
|
|
1665
|
+
try {
|
|
1666
|
+
return this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1667
|
+
} finally {
|
|
1668
|
+
this.resolvingConcats.delete(v);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1505
1671
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1506
1672
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1507
1673
|
const hv = v;
|
|
@@ -1515,15 +1681,12 @@ var SubstitutionResolver = class {
|
|
|
1515
1681
|
}
|
|
1516
1682
|
return hv;
|
|
1517
1683
|
}
|
|
1518
|
-
segmentsEqual(a, b) {
|
|
1519
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1520
|
-
}
|
|
1521
1684
|
resolveSubst(s, scope) {
|
|
1522
|
-
const key = segmentsToKey(s.segments);
|
|
1685
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1523
1686
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1524
1687
|
if (this.resolving.has(key)) {
|
|
1525
1688
|
let prior;
|
|
1526
|
-
if (s.
|
|
1689
|
+
if (s.segments.length > 1) {
|
|
1527
1690
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1528
1691
|
const parentScope = lookupResObj(
|
|
1529
1692
|
this.root,
|
|
@@ -1555,29 +1718,31 @@ var SubstitutionResolver = class {
|
|
|
1555
1718
|
try {
|
|
1556
1719
|
const found = lookupPath(this.root, s.segments);
|
|
1557
1720
|
if (found !== void 0) {
|
|
1558
|
-
if (
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
prior = parentScope?.priorValues.get(leafSeg);
|
|
1571
|
-
} else {
|
|
1572
|
-
const rootSeg = s.segments[0]?.text ?? "";
|
|
1573
|
-
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1574
|
-
}
|
|
1575
|
-
if (prior !== void 0) {
|
|
1576
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1577
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1578
|
-
return result2;
|
|
1579
|
-
}
|
|
1721
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1722
|
+
let prior;
|
|
1723
|
+
if (s.segments.length > 1) {
|
|
1724
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1725
|
+
const parentScope = lookupResObj(
|
|
1726
|
+
this.root,
|
|
1727
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1728
|
+
);
|
|
1729
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1730
|
+
} else {
|
|
1731
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1732
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1580
1733
|
}
|
|
1734
|
+
if (prior !== void 0) {
|
|
1735
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1736
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1737
|
+
return result2;
|
|
1738
|
+
}
|
|
1739
|
+
if (s.optional) return void 0;
|
|
1740
|
+
throw new ResolveError(
|
|
1741
|
+
`could not resolve substitution: \${${key}}`,
|
|
1742
|
+
key,
|
|
1743
|
+
s.line,
|
|
1744
|
+
s.col
|
|
1745
|
+
);
|
|
1581
1746
|
}
|
|
1582
1747
|
let result = this.resolveVal(found, scope);
|
|
1583
1748
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1606,6 +1771,21 @@ var SubstitutionResolver = class {
|
|
|
1606
1771
|
if (result !== void 0) this.cache.set(key, result);
|
|
1607
1772
|
return result;
|
|
1608
1773
|
}
|
|
1774
|
+
if (s.listSuffix) {
|
|
1775
|
+
const result = this.resolveEnvList(s);
|
|
1776
|
+
if (result !== void 0) {
|
|
1777
|
+
this.cache.set(key, result);
|
|
1778
|
+
return result;
|
|
1779
|
+
}
|
|
1780
|
+
if (s.optional) return void 0;
|
|
1781
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1782
|
+
throw new ResolveError(
|
|
1783
|
+
`could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1784
|
+
key,
|
|
1785
|
+
s.line,
|
|
1786
|
+
s.col
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1609
1789
|
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1610
1790
|
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1611
1791
|
if (envVal !== void 0) {
|
|
@@ -1624,7 +1804,42 @@ var SubstitutionResolver = class {
|
|
|
1624
1804
|
this.resolving.delete(key);
|
|
1625
1805
|
}
|
|
1626
1806
|
}
|
|
1627
|
-
|
|
1807
|
+
/**
|
|
1808
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1809
|
+
*
|
|
1810
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1811
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1812
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1813
|
+
*
|
|
1814
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1815
|
+
* none do (caller applies optional/required handling).
|
|
1816
|
+
*/
|
|
1817
|
+
resolveEnvList(s) {
|
|
1818
|
+
const bases = this.candidateBases(s);
|
|
1819
|
+
for (const base of bases) {
|
|
1820
|
+
const key0 = base + "_0";
|
|
1821
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1822
|
+
const items = [];
|
|
1823
|
+
for (let i = 0; ; i++) {
|
|
1824
|
+
const k = base + "_" + i;
|
|
1825
|
+
if (!(k in this.opts.env)) break;
|
|
1826
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1827
|
+
}
|
|
1828
|
+
return { kind: "array", items };
|
|
1829
|
+
}
|
|
1830
|
+
return void 0;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1834
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1835
|
+
*/
|
|
1836
|
+
candidateBases(s) {
|
|
1837
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1838
|
+
if (s.prefixLen === 0) return [full];
|
|
1839
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1840
|
+
return [full, bare];
|
|
1841
|
+
}
|
|
1842
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1628
1843
|
const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
|
|
1629
1844
|
if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1630
1845
|
if (resolved.length === 1) return resolved[0];
|
|
@@ -1642,27 +1857,45 @@ var SubstitutionResolver = class {
|
|
|
1642
1857
|
if (converted !== null) {
|
|
1643
1858
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1644
1859
|
}
|
|
1645
|
-
|
|
1860
|
+
throw new ResolveError(
|
|
1861
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
1862
|
+
"",
|
|
1863
|
+
line,
|
|
1864
|
+
col
|
|
1865
|
+
);
|
|
1646
1866
|
}
|
|
1647
1867
|
if (left.kind === "object" && right.kind === "array") {
|
|
1648
1868
|
const converted = numericObjectToArray(left);
|
|
1649
1869
|
if (converted !== null) {
|
|
1650
1870
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1651
1871
|
}
|
|
1652
|
-
|
|
1872
|
+
throw new ResolveError(
|
|
1873
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
1874
|
+
"",
|
|
1875
|
+
line,
|
|
1876
|
+
col
|
|
1877
|
+
);
|
|
1653
1878
|
}
|
|
1654
1879
|
if (left.kind === "array" && right.kind === "array") {
|
|
1655
1880
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1656
1881
|
}
|
|
1657
|
-
if (left.kind === "array") {
|
|
1658
|
-
|
|
1882
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
1883
|
+
throw new ResolveError(
|
|
1884
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1885
|
+
"",
|
|
1886
|
+
line,
|
|
1887
|
+
col
|
|
1888
|
+
);
|
|
1659
1889
|
}
|
|
1660
|
-
if (right.kind === "
|
|
1661
|
-
|
|
1890
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
1891
|
+
throw new ResolveError(
|
|
1892
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
1893
|
+
"",
|
|
1894
|
+
line,
|
|
1895
|
+
col
|
|
1896
|
+
);
|
|
1662
1897
|
}
|
|
1663
|
-
|
|
1664
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1665
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
1898
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1666
1899
|
};
|
|
1667
1900
|
const [head, ...tail] = nonSep;
|
|
1668
1901
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1707,6 +1940,7 @@ async function defaultReadFile(filePath) {
|
|
|
1707
1940
|
}
|
|
1708
1941
|
function buildResolveContext(input, opts) {
|
|
1709
1942
|
const tokens = tokenize(input);
|
|
1943
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1710
1944
|
const ast = parseTokens(tokens);
|
|
1711
1945
|
const resolveOpts = {
|
|
1712
1946
|
env: getEnv(opts),
|