@intentius/chant-lexicon-terraform 0.124.31 → 0.124.33

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.
Files changed (149) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.js +111 -0
  2. package/dist/__fixtures__/terragrunt/record.js +57 -0
  3. package/dist/behaviour/index.js +6 -0
  4. package/dist/behaviour/kinds.js +194 -0
  5. package/dist/behaviour/predict.js +297 -0
  6. package/dist/behaviour/reference-catalog.js +178 -0
  7. package/dist/behaviour/request.js +482 -0
  8. package/dist/change-set.js +234 -0
  9. package/dist/changed-roots.js +97 -0
  10. package/dist/codegen/docs-cli.js +3 -0
  11. package/dist/codegen/docs.js +44 -0
  12. package/dist/codegen/generate-cli.js +9 -0
  13. package/dist/codegen/generate.js +59 -0
  14. package/dist/codegen/package.js +43 -0
  15. package/dist/commands.js +106 -0
  16. package/dist/components/capability-plugin.d.ts +38 -2
  17. package/dist/components/capability-plugin.d.ts.map +1 -1
  18. package/dist/components/capability-plugin.js +260 -0
  19. package/dist/composites/terraform-adopt-op.d.ts +2 -0
  20. package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
  21. package/dist/composites/terraform-adopt-op.js +153 -0
  22. package/dist/composites/terraform-apply-op.d.ts +2 -0
  23. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  24. package/dist/composites/terraform-apply-op.js +184 -0
  25. package/dist/composites/terraform-pin-rollout-op.js +45 -0
  26. package/dist/composites/terraform-watch-op.js +219 -0
  27. package/dist/config.js +118 -0
  28. package/dist/describe-resources.js +984 -0
  29. package/dist/generated/index.js +1 -0
  30. package/dist/hcl/descend.js +208 -0
  31. package/dist/hcl/edges.js +287 -0
  32. package/dist/hcl/jsonencode.js +442 -0
  33. package/dist/hcl/marker.js +212 -0
  34. package/dist/hcl/parse.js +372 -0
  35. package/dist/hcl/references.js +156 -0
  36. package/dist/hcl/roots.js +98 -0
  37. package/dist/hcl/suppressions.js +189 -0
  38. package/dist/hcl/value.js +72 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +46 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/lint/audit-catalog.js +501 -0
  44. package/dist/lint/audit-lineage.js +442 -0
  45. package/dist/lint/post-synth/blocks.js +87 -0
  46. package/dist/lint/post-synth/fixtures/load.js +94 -0
  47. package/dist/lint/post-synth/flat-attributes.js +135 -0
  48. package/dist/lint/post-synth/index.js +90 -0
  49. package/dist/lint/post-synth/module-source.js +153 -0
  50. package/dist/lint/post-synth/scope.js +104 -0
  51. package/dist/lint/post-synth/terragrunt.js +63 -0
  52. package/dist/lint/post-synth/tf001.js +138 -0
  53. package/dist/lint/post-synth/tf002.js +143 -0
  54. package/dist/lint/post-synth/tf003.js +60 -0
  55. package/dist/lint/post-synth/tf004.js +54 -0
  56. package/dist/lint/post-synth/tf005.js +68 -0
  57. package/dist/lint/post-synth/tf006.js +43 -0
  58. package/dist/lint/post-synth/tf007.js +66 -0
  59. package/dist/lint/post-synth/tf008.js +64 -0
  60. package/dist/lint/post-synth/tf009.js +49 -0
  61. package/dist/lint/post-synth/tf010.js +40 -0
  62. package/dist/lint/post-synth/tf011.js +38 -0
  63. package/dist/lint/post-synth/tf012.js +36 -0
  64. package/dist/lint/post-synth/tf013.js +53 -0
  65. package/dist/lint/post-synth/tf014.js +61 -0
  66. package/dist/lint/post-synth/tf015.js +62 -0
  67. package/dist/lint/post-synth/tf016.js +66 -0
  68. package/dist/lint/post-synth/tf017.js +39 -0
  69. package/dist/lint/post-synth/tf018.js +63 -0
  70. package/dist/lint/post-synth/tf019.js +60 -0
  71. package/dist/lint/post-synth/tf020.js +103 -0
  72. package/dist/lint/post-synth/tf021.js +117 -0
  73. package/dist/lint/post-synth/tf022.js +51 -0
  74. package/dist/lint/post-synth/tf024.js +62 -0
  75. package/dist/lint/post-synth/tf025.js +83 -0
  76. package/dist/lint/post-synth/tf026.js +74 -0
  77. package/dist/lint/post-synth/tf027.js +66 -0
  78. package/dist/lint/post-synth/tf028.js +83 -0
  79. package/dist/lint/post-synth/tf029.js +73 -0
  80. package/dist/lint/post-synth/tf030.js +253 -0
  81. package/dist/lint/post-synth/tf031.js +332 -0
  82. package/dist/lint/post-synth/tf032.js +201 -0
  83. package/dist/lint/post-synth/tf033.js +126 -0
  84. package/dist/lint/post-synth/tf034.js +45 -0
  85. package/dist/lint/post-synth/tf035.js +63 -0
  86. package/dist/lint/post-synth/tf036.js +97 -0
  87. package/dist/lint/post-synth/tf037.js +80 -0
  88. package/dist/lint/post-synth/tf038.js +63 -0
  89. package/dist/lint/post-synth/tf039.js +55 -0
  90. package/dist/lint/post-synth/tf040.js +67 -0
  91. package/dist/lint/post-synth/tf041.js +53 -0
  92. package/dist/lint/post-synth/tf042.js +38 -0
  93. package/dist/lint/post-synth/tf043.js +45 -0
  94. package/dist/lint/post-synth/tf044.js +45 -0
  95. package/dist/lint/post-synth/tf045.js +44 -0
  96. package/dist/lint/rules/fixtures/load.js +50 -0
  97. package/dist/lint/rules/index.js +4 -0
  98. package/dist/lint/rules/plan-before-apply.js +136 -0
  99. package/dist/lint/secret-shape.js +149 -0
  100. package/dist/live-ownership.js +57 -0
  101. package/dist/lsp/completions.js +95 -0
  102. package/dist/lsp/context.js +358 -0
  103. package/dist/lsp/hover.js +45 -0
  104. package/dist/lsp/option-keys.js +93 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.js +30 -0
  107. package/dist/op/activities/live-detect.js +143 -0
  108. package/dist/op/activities/pin-rollout.js +45 -0
  109. package/dist/op/activities/terraform.js +1270 -0
  110. package/dist/op/activity-contracts.js +162 -0
  111. package/dist/op/adoption.js +272 -0
  112. package/dist/op/builders.js +125 -0
  113. package/dist/op/resolve-root-mode.js +77 -0
  114. package/dist/package-cli.js +16 -0
  115. package/dist/pin/edit-ts.js +76 -0
  116. package/dist/pin/edit.js +118 -0
  117. package/dist/pin/forge.js +65 -0
  118. package/dist/pin/index.js +16 -0
  119. package/dist/pin/rollout.d.ts.map +1 -1
  120. package/dist/pin/rollout.js +382 -0
  121. package/dist/pin/scan.js +287 -0
  122. package/dist/pin/source.js +171 -0
  123. package/dist/pin/waves.js +109 -0
  124. package/dist/plan-digest.js +67 -0
  125. package/dist/plugin.js +323 -0
  126. package/dist/serializer.js +23 -0
  127. package/dist/state-ownership.js +29 -0
  128. package/dist/terragrunt/affected.d.ts.map +1 -1
  129. package/dist/terragrunt/affected.js +290 -0
  130. package/dist/terragrunt/index.js +10 -0
  131. package/dist/terragrunt/mocks.js +270 -0
  132. package/dist/terragrunt/run.js +264 -0
  133. package/dist/terragrunt/units.js +195 -0
  134. package/dist/terragrunt/wave.js +125 -0
  135. package/dist/validate-cli.js +4 -0
  136. package/dist/validate.js +31 -0
  137. package/package.json +10 -10
  138. package/src/codegen/package.ts +2 -2
  139. package/src/components/capability-plugin.test.ts +97 -2
  140. package/src/components/capability-plugin.ts +117 -12
  141. package/src/components/pr-loop.tofu.test.ts +51 -11
  142. package/src/composites/terraform-adopt-op.test.ts +6 -0
  143. package/src/composites/terraform-adopt-op.ts +7 -2
  144. package/src/composites/terraform-apply-op.test.ts +6 -0
  145. package/src/composites/terraform-apply-op.ts +7 -2
  146. package/src/index.ts +1 -0
  147. package/src/pin/rollout.ts +2 -1
  148. package/src/terragrunt/affected.test.ts +11 -0
  149. package/src/terragrunt/affected.ts +2 -1
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Read the argument of a `jsonencode(...)` call as structure, when it is
3
+ * literal (chant #2286, shared with #2287).
4
+ *
5
+ * hcl2json does not evaluate expressions. `policy = jsonencode({...})` and
6
+ * `container_definitions = jsonencode([...])` both reach a block body as ONE
7
+ * string holding the expression's source text, wrapped as an interpolation:
8
+ *
9
+ * ```
10
+ * "${jsonencode({\n Version = \"2012-10-17\"\n Statement = [{ Action = \"*\" }]\n })}"
11
+ * ```
12
+ *
13
+ * The text inside is HCL object syntax, not JSON (`=` or `:` separators, bare
14
+ * keys, newline-separated items, `#` comments). A rule that wants the policy
15
+ * or the container definitions has to read that text itself. This file does
16
+ * so, narrowly: it accepts literal objects, tuples, quoted strings, numbers,
17
+ * `true`, `false` and `null`, and nothing else. Any expression in the
18
+ * argument (a reference, a function call, a for-expression, a splat, a
19
+ * conditional, an operator, a template interpolation inside a string, a
20
+ * heredoc, a parenthesised key) makes the whole read `not-determined`, with a
21
+ * reason naming what was found. It never evaluates anything, so it can never
22
+ * return a value Terraform would not also produce from the same text.
23
+ *
24
+ * Three answers:
25
+ * - `literal`: the value is a single whole-string `${jsonencode(...)}` and
26
+ * its argument is fully literal. `value` is the plain JS value
27
+ * `jsonencode` would encode.
28
+ * - `not-determined`: the value is a `${jsonencode(...)}` call, but the
29
+ * argument holds something this reader does not evaluate.
30
+ * - `not-jsonencode`: the value is not a `${jsonencode(...)}` call at all
31
+ * (a plain string, a reference, `file(...)`, a number, absent). Callers
32
+ * decide what that means for their attribute; a plain string, for example,
33
+ * may itself be JSON from a heredoc.
34
+ *
35
+ * The whole-string test is shape-based: the string starts `${jsonencode(` and
36
+ * ends `)}`. A string that matches that shape but is really a template with
37
+ * two interpolations (`${jsonencode(a)}-${jsonencode(b)}`) fails the literal
38
+ * parse at the point where the first call closes early, and reads as
39
+ * `not-determined`, never as `literal`.
40
+ */
41
+ /** Marks a value position `readJsonencode(raw, { unknownLeaves: true })` could not read. */
42
+ export const UNKNOWN = Symbol("jsonencode.unknown");
43
+ /**
44
+ * Marks an object, read with `unknownLeaves`, that had members whose KEY is an
45
+ * expression (`"router-${var.n}.rule" = ...`, `(var.k) = ...`). Those members
46
+ * are dropped from the object and their reasons listed here, so a reader can
47
+ * tell "this key is absent" from "some key here could not be read".
48
+ */
49
+ export const UNKNOWN_KEYS = Symbol("jsonencode.unknownKeys");
50
+ /** Whether `obj` is an object that dropped one or more members with an expression key. */
51
+ export function hasUnknownKeys(obj) {
52
+ return (typeof obj === "object" &&
53
+ obj !== null &&
54
+ Array.isArray(obj[UNKNOWN_KEYS]) &&
55
+ obj[UNKNOWN_KEYS].length > 0);
56
+ }
57
+ /** Whether `v` is a value position the reader skipped. */
58
+ export function isUnknown(v) {
59
+ return typeof v === "object" && v !== null && v[UNKNOWN] === true;
60
+ }
61
+ const PREFIX = /^\$\{\s*jsonencode\s*\(/;
62
+ const SUFFIX = /\)\s*\}$/;
63
+ /** Read `raw` (an hcl2json attribute value) as a `jsonencode(...)` call. */
64
+ export function readJsonencode(raw, options = {}) {
65
+ if (typeof raw !== "string")
66
+ return { kind: "not-jsonencode" };
67
+ const prefix = PREFIX.exec(raw);
68
+ if (!prefix || !SUFFIX.test(raw))
69
+ return { kind: "not-jsonencode" };
70
+ const reader = new Reader(raw, prefix[0].length, options.unknownLeaves === true);
71
+ try {
72
+ const value = reader.value(0);
73
+ reader.skipSpace();
74
+ // The argument must end at the call's own `)`, then the interpolation's `}`.
75
+ reader.expect(")", "a second argument or an expression after the literal");
76
+ reader.skipSpace();
77
+ reader.expect("}", "text after the jsonencode call");
78
+ if (!reader.atEnd())
79
+ throw new NotLiteral("text after the jsonencode call");
80
+ return { kind: "literal", value };
81
+ }
82
+ catch (err) {
83
+ if (err instanceof NotLiteral)
84
+ return { kind: "not-determined", reason: err.message };
85
+ throw err;
86
+ }
87
+ }
88
+ class NotLiteral extends Error {
89
+ }
90
+ /** Damage to the shape itself: never recovered by skipping a leaf. */
91
+ class Structural extends NotLiteral {
92
+ }
93
+ /** An object key that is an expression; recoverable by dropping the member under `unknownLeaves`. */
94
+ class KeyNotLiteral extends Structural {
95
+ }
96
+ const IDENT = /[A-Za-z_][A-Za-z0-9_-]*/y;
97
+ const NUMBER = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
98
+ /** A cursor over the source text of the jsonencode argument. */
99
+ class Reader {
100
+ constructor(src, pos, unknownLeaves = false) {
101
+ this.src = src;
102
+ this.pos = pos;
103
+ this.unknownLeaves = unknownLeaves;
104
+ }
105
+ atEnd() {
106
+ return this.pos >= this.src.length;
107
+ }
108
+ peek() {
109
+ return this.src[this.pos] ?? "";
110
+ }
111
+ expect(ch, what) {
112
+ if (this.peek() !== ch)
113
+ throw new NotLiteral(what === "" ? `expected "${ch}"` : what);
114
+ this.pos++;
115
+ }
116
+ /** Whitespace, newlines and the three HCL comment forms. */
117
+ skipSpace(newlines = true) {
118
+ for (;;) {
119
+ const c = this.peek();
120
+ if (c === " " || c === "\t" || c === "\r" || (newlines && c === "\n")) {
121
+ this.pos++;
122
+ }
123
+ else if (c === "#" || (c === "/" && this.src[this.pos + 1] === "/")) {
124
+ const end = this.src.indexOf("\n", this.pos);
125
+ this.pos = end === -1 ? this.src.length : end;
126
+ }
127
+ else if (c === "/" && this.src[this.pos + 1] === "*") {
128
+ const end = this.src.indexOf("*/", this.pos + 2);
129
+ if (end === -1)
130
+ throw new NotLiteral("an unterminated comment");
131
+ this.pos = end + 2;
132
+ }
133
+ else {
134
+ return;
135
+ }
136
+ }
137
+ }
138
+ /** One value. `depth` is 0 for the whole argument, which is never skipped. */
139
+ value(depth) {
140
+ this.skipSpace();
141
+ if (!this.unknownLeaves || depth === 0)
142
+ return this.literal(depth);
143
+ const start = this.pos;
144
+ try {
145
+ return this.literal(depth);
146
+ }
147
+ catch (err) {
148
+ if (!(err instanceof NotLiteral) || err instanceof Structural)
149
+ throw err;
150
+ this.pos = start;
151
+ this.skipExpression();
152
+ return { [UNKNOWN]: true, reason: err.message, source: this.src.slice(start, this.pos).trim() };
153
+ }
154
+ }
155
+ literal(depth) {
156
+ const c = this.peek();
157
+ let v;
158
+ if (c === "{")
159
+ v = this.object(depth + 1);
160
+ else if (c === "[")
161
+ v = this.tuple(depth + 1);
162
+ else if (c === '"')
163
+ v = this.string();
164
+ else if (c === "<" && this.src.startsWith("<<", this.pos))
165
+ throw new NotLiteral("a heredoc");
166
+ else if (c === "(")
167
+ throw new NotLiteral("a parenthesised expression");
168
+ else if (c === "-" || (c >= "0" && c <= "9"))
169
+ v = this.number();
170
+ else
171
+ v = this.keyword();
172
+ this.afterValue();
173
+ return v;
174
+ }
175
+ /**
176
+ * A literal is complete only if what follows it closes or separates. An
177
+ * operator, a conditional, an attribute access or an index after it makes
178
+ * the whole thing an expression.
179
+ */
180
+ afterValue() {
181
+ this.skipSpace(false);
182
+ const c = this.peek();
183
+ if (c === "" || c === "\n" || c === "," || c === "}" || c === "]" || c === ")")
184
+ return;
185
+ const next = this.src[this.pos + 1];
186
+ if (c === "#" || (c === "/" && (next === "/" || next === "*")))
187
+ return; // a comment, consumed by the caller's skipSpace
188
+ if (c === "?")
189
+ throw new NotLiteral("a conditional expression");
190
+ if (c === "." || c === "[")
191
+ throw new NotLiteral("an attribute access, index or splat");
192
+ throw new NotLiteral(`an operator or expression ("${c}")`);
193
+ }
194
+ object(depth) {
195
+ this.expect("{", "");
196
+ const out = {};
197
+ for (;;) {
198
+ this.skipSpace();
199
+ if (this.peek() === "}") {
200
+ this.pos++;
201
+ return out;
202
+ }
203
+ const quoted = this.peek() === '"';
204
+ const memberStart = this.pos;
205
+ let key;
206
+ try {
207
+ key = this.key();
208
+ }
209
+ catch (err) {
210
+ if (!(err instanceof KeyNotLiteral) || !this.unknownLeaves)
211
+ throw err;
212
+ // Drop the whole member: the skip runs from the key through its value
213
+ // to the separator, balancing brackets and quotes on the way.
214
+ this.pos = memberStart;
215
+ this.skipExpression();
216
+ const bag = out;
217
+ (bag[UNKNOWN_KEYS] ?? (bag[UNKNOWN_KEYS] = [])).push(err.message);
218
+ this.skipSpace(false);
219
+ if (this.peek() === ",")
220
+ this.pos++;
221
+ continue;
222
+ }
223
+ this.skipSpace(false);
224
+ const sep = this.peek();
225
+ if (sep !== "=" && sep !== ":") {
226
+ // A for-expression is a VALUE: under unknownLeaves the enclosing
227
+ // value() skips it as a leaf; as the whole argument it is not determined.
228
+ if (!quoted && key === "for")
229
+ throw new NotLiteral("a for-expression");
230
+ throw new Structural(`an expression where "${key}" needs = or :`);
231
+ }
232
+ this.pos++;
233
+ out[key] = this.value(depth);
234
+ this.skipSpace(false);
235
+ if (this.peek() === ",")
236
+ this.pos++;
237
+ }
238
+ }
239
+ key() {
240
+ const c = this.peek();
241
+ if (c === '"') {
242
+ try {
243
+ return this.string();
244
+ }
245
+ catch (err) {
246
+ if (err instanceof NotLiteral)
247
+ throw new KeyNotLiteral(`an object key holding ${err.message.replace(/^a string with /, "")}`);
248
+ throw err;
249
+ }
250
+ }
251
+ if (c === "(")
252
+ throw new KeyNotLiteral("a computed object key");
253
+ IDENT.lastIndex = this.pos;
254
+ const m = IDENT.exec(this.src);
255
+ if (!m)
256
+ throw new Structural(`an unexpected "${c}" where an object key belongs`);
257
+ this.pos += m[0].length;
258
+ return m[0];
259
+ }
260
+ tuple(depth) {
261
+ this.expect("[", "");
262
+ this.skipSpace();
263
+ if (/^for\s/.test(this.src.slice(this.pos, this.pos + 4)))
264
+ throw new NotLiteral("a for-expression");
265
+ const out = [];
266
+ for (;;) {
267
+ this.skipSpace();
268
+ if (this.peek() === "]") {
269
+ this.pos++;
270
+ return out;
271
+ }
272
+ out.push(this.value(depth));
273
+ this.skipSpace();
274
+ if (this.peek() === ",")
275
+ this.pos++;
276
+ else if (this.peek() !== "]")
277
+ throw new NotLiteral("a tuple item followed by an expression");
278
+ }
279
+ }
280
+ /** A quoted HCL string. A `${` or `%{` (not escaped as `$${`/`%%{`) is a template. */
281
+ string() {
282
+ this.expect('"', "");
283
+ let out = "";
284
+ for (;;) {
285
+ const c = this.peek();
286
+ if (c === "")
287
+ throw new NotLiteral("an unterminated string");
288
+ if (c === "\n")
289
+ throw new NotLiteral("a newline inside a quoted string");
290
+ if (c === '"') {
291
+ this.pos++;
292
+ return out;
293
+ }
294
+ if ((c === "$" || c === "%") && this.src[this.pos + 1] === c && this.src[this.pos + 2] === "{") {
295
+ out += `${c}{`;
296
+ this.pos += 3;
297
+ continue;
298
+ }
299
+ if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
300
+ throw new NotLiteral(c === "$" ? "a string with a ${...} interpolation" : "a string with a %{...} directive");
301
+ }
302
+ if (c === "\\") {
303
+ out += this.escape();
304
+ continue;
305
+ }
306
+ out += c;
307
+ this.pos++;
308
+ }
309
+ }
310
+ escape() {
311
+ const e = this.src[this.pos + 1] ?? "";
312
+ this.pos += 2;
313
+ switch (e) {
314
+ case "n":
315
+ return "\n";
316
+ case "r":
317
+ return "\r";
318
+ case "t":
319
+ return "\t";
320
+ case '"':
321
+ return '"';
322
+ case "\\":
323
+ return "\\";
324
+ case "u":
325
+ case "U": {
326
+ const len = e === "u" ? 4 : 8;
327
+ const hex = this.src.slice(this.pos, this.pos + len);
328
+ if (!/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== len)
329
+ throw new NotLiteral("a malformed unicode escape");
330
+ this.pos += len;
331
+ return String.fromCodePoint(parseInt(hex, 16));
332
+ }
333
+ default:
334
+ throw new NotLiteral(`an unknown string escape "\\${e}"`);
335
+ }
336
+ }
337
+ number() {
338
+ NUMBER.lastIndex = this.pos;
339
+ const m = NUMBER.exec(this.src);
340
+ if (!m)
341
+ throw new NotLiteral("a unary operator on an expression");
342
+ this.pos += m[0].length;
343
+ return Number(m[0]);
344
+ }
345
+ keyword() {
346
+ IDENT.lastIndex = this.pos;
347
+ const m = IDENT.exec(this.src);
348
+ if (!m)
349
+ throw new NotLiteral(`an unexpected "${this.peek()}"`);
350
+ const word = m[0];
351
+ this.pos += word.length;
352
+ if (word === "true")
353
+ return true;
354
+ if (word === "false")
355
+ return false;
356
+ if (word === "null")
357
+ return null;
358
+ this.skipSpace(false);
359
+ if (this.peek() === "(")
360
+ throw new NotLiteral(`a function call (${word}(...))`);
361
+ if (word === "for")
362
+ throw new NotLiteral("a for-expression");
363
+ return this.reference(word);
364
+ }
365
+ /**
366
+ * Skip one value expression from the cursor: balance `()[]{}` and quoted
367
+ * strings (with their `${...}` interpolations), stop at a `,`, a newline, a
368
+ * comment or a closing bracket at depth 0. A heredoc is skipped to its
369
+ * closing marker line.
370
+ */
371
+ skipExpression() {
372
+ let depth = 0;
373
+ while (!this.atEnd()) {
374
+ const c = this.peek();
375
+ if (depth === 0) {
376
+ if (c === "," || c === "\n" || c === "}" || c === "]" || c === ")" || c === "#")
377
+ return;
378
+ if (c === "/" && (this.src[this.pos + 1] === "/" || this.src[this.pos + 1] === "*"))
379
+ return;
380
+ }
381
+ if (c === '"')
382
+ this.skipQuoted();
383
+ else if (c === "<" && this.src.startsWith("<<", this.pos))
384
+ this.skipHeredoc();
385
+ else {
386
+ if (c === "(" || c === "[" || c === "{")
387
+ depth++;
388
+ else if (c === ")" || c === "]" || c === "}")
389
+ depth--;
390
+ this.pos++;
391
+ }
392
+ }
393
+ }
394
+ /** Skip a quoted string, including any `${...}` interpolations (which may hold strings). */
395
+ skipQuoted() {
396
+ this.pos++;
397
+ while (!this.atEnd()) {
398
+ const c = this.peek();
399
+ if (c === "\\")
400
+ this.pos += 2;
401
+ else if (c === '"') {
402
+ this.pos++;
403
+ return;
404
+ }
405
+ else if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
406
+ this.pos += 2;
407
+ let depth = 1;
408
+ while (!this.atEnd() && depth > 0) {
409
+ const d = this.peek();
410
+ if (d === '"') {
411
+ this.skipQuoted();
412
+ continue;
413
+ }
414
+ if (d === "{")
415
+ depth++;
416
+ else if (d === "}")
417
+ depth--;
418
+ this.pos++;
419
+ }
420
+ }
421
+ else
422
+ this.pos++;
423
+ }
424
+ throw new Structural("an unterminated string");
425
+ }
426
+ skipHeredoc() {
427
+ const head = /<<-?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\n/y;
428
+ head.lastIndex = this.pos;
429
+ const m = head.exec(this.src);
430
+ if (!m)
431
+ throw new Structural("a malformed heredoc");
432
+ const close = new RegExp(`^[ \\t]*${m[1]}[ \\t]*$`, "m");
433
+ const rest = this.src.slice(this.pos + m[0].length);
434
+ const end = close.exec(rest);
435
+ if (!end)
436
+ throw new Structural("an unterminated heredoc");
437
+ this.pos += m[0].length + end.index + end[0].length;
438
+ }
439
+ reference(word) {
440
+ throw new NotLiteral(`a reference (${word}${this.peek() === "." ? "..." : ""})`);
441
+ }
442
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * choudoufu's live resource markers, as chant reads them (#2104).
3
+ *
4
+ * On a live root the ownership record is not a state file and not a chant
5
+ * stamp: it is two AWS tags on the resource itself, `tofu-estate` and
6
+ * `tofu-address`, specified in `live/MARKERS.md` in
7
+ * https://github.com/INTENTIUS/choudoufu. That file calls itself the one
8
+ * integration surface external tools may rely on, so this module implements
9
+ * exactly what it specifies and nothing beyond it. The spec's own tables are
10
+ * this module's tests (`./marker.test.ts`).
11
+ *
12
+ * ## Escape, then compare. Never decode a tag blind
13
+ *
14
+ * A canonical terraform address uses `[`, `]` and `"` to write an instance
15
+ * key, and AWS allows none of the three in a tag value. So an address is
16
+ * escaped before it is written, and the spec's comparison rule is one
17
+ * sentence: escape the address you already know and compare strings. The
18
+ * reverse, decoding a tag and hoping to recover the address, is ambiguous
19
+ * (`this[2]` and `this["2"]` escape alike) and the spec says not to do it
20
+ * where a verdict depends on the answer.
21
+ *
22
+ * The rule itself, from "Escaping rule":
23
+ *
24
+ * 1. escape the content of every instance key first (see
25
+ * {@link escapeForEachKey}); a `count` index is only ever digits, so
26
+ * this is a no-op for one;
27
+ * 2. replace every `[` with `:`;
28
+ * 3. delete every `]`;
29
+ * 4. delete every `"`.
30
+ *
31
+ * Step 1 first is what makes a `:` in an escaped value always mean "an index
32
+ * starts here": a key's own `.` and `:` are already gone by the time steps
33
+ * 2 to 4 run.
34
+ *
35
+ * ## Three escapings on the declared side
36
+ *
37
+ * The key escaping gained a layer twice, and neither change rewrote the
38
+ * markers already on live resources. A marker stamped before choudoufu's
39
+ * issue #178 escaped no key at all; one stamped between #178 and #210
40
+ * doubled `@`, `.` and `:` but did not carry out-of-charset characters into
41
+ * the AWS-legal set. So {@link addressMatches} escapes the declared address
42
+ * three ways and accepts any of them, which is what `AddressMatches` does in
43
+ * choudoufu itself. Writing always uses {@link escapeAddress}, the current
44
+ * form.
45
+ */
46
+ /** `tofu-estate`: the estate that owns the resource. The entire ownership claim. */
47
+ export const MARKER_TAG_ESTATE = "tofu-estate";
48
+ /** `tofu-address`: the resource's escaped canonical address, or its first 256-character chunk. */
49
+ export const MARKER_TAG_ADDRESS = "tofu-address";
50
+ /** `tofu-slot`: the stable, opaque cardinality slot a `count` instance carries. */
51
+ export const MARKER_TAG_SLOT = "tofu-slot";
52
+ /**
53
+ * `tofu-address-2` through `tofu-address-4`, in order. A reader concatenates
54
+ * `tofu-address` and whichever of these are present; they are never
55
+ * meaningful on their own.
56
+ */
57
+ export const MARKER_TAG_ADDRESS_CONTINUATIONS = [
58
+ "tofu-address-2",
59
+ "tofu-address-3",
60
+ "tofu-address-4",
61
+ ];
62
+ /** The characters AWS allows in a tag value beside letters, digits and space. */
63
+ const AWS_LEGAL_PUNCTUATION = new Set(["+", "-", "=", ".", "_", ":", "/", "@"]);
64
+ /** The escape introducer of the out-of-charset layer (choudoufu issue #210). */
65
+ const INTRODUCER = "+";
66
+ function isAwsLegal(ch) {
67
+ return /^[A-Za-z0-9 ]$/.test(ch) || AWS_LEGAL_PUNCTUATION.has(ch);
68
+ }
69
+ /**
70
+ * Carry every character outside the AWS-legal set into it: `+` doubles
71
+ * because it is the introducer, and anything else becomes `+` followed by its
72
+ * Unicode code point in six uppercase hex digits. `a(b)` becomes
73
+ * `a+000028b+000029`; `plus+one` becomes `plus++one`.
74
+ */
75
+ function escapeOutOfCharset(key) {
76
+ let out = "";
77
+ for (const ch of key) {
78
+ if (ch === INTRODUCER)
79
+ out += "++";
80
+ else if (isAwsLegal(ch))
81
+ out += ch;
82
+ else
83
+ out += INTRODUCER + ch.codePointAt(0).toString(16).toUpperCase().padStart(6, "0");
84
+ }
85
+ return out;
86
+ }
87
+ /**
88
+ * The `@` / `.` / `:` doubling (choudoufu issue #178), applied in that order
89
+ * so every `@` these steps introduce is never itself doubled again: `@`
90
+ * becomes `@@`, then `.` becomes `@d`, then `:` becomes `@c`.
91
+ */
92
+ function escapeReservedPunctuation(key) {
93
+ let out = "";
94
+ for (const ch of key) {
95
+ if (ch === "@")
96
+ out += "@@";
97
+ else if (ch === ".")
98
+ out += "@d";
99
+ else if (ch === ":")
100
+ out += "@c";
101
+ else
102
+ out += ch;
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * Escape one `for_each` instance key, both layers in the specified order:
108
+ * out-of-charset escaping first, then the `@` / `.` / `:` doubling over its
109
+ * output. Order is load-bearing: the first layer's introducer is `+`, none of
110
+ * the three characters the second layer scans for, so the second never
111
+ * mistakes the first's output for its own escape sequences.
112
+ *
113
+ * Six characters cannot be carried at all and are refused at choudoufu's own
114
+ * lint rather than here (`"`, `\`, any non-printable, `$`, `%`, `[`, `]`);
115
+ * this function escapes what it is given without judging it, because chant
116
+ * never writes a marker, and a key chant meets in a `tofu-address` tag has
117
+ * already passed that lint.
118
+ */
119
+ export function escapeForEachKey(key) {
120
+ return escapeReservedPunctuation(escapeOutOfCharset(key));
121
+ }
122
+ /** The pre-#210 key escaping: the doubling alone, with no out-of-charset layer. */
123
+ function escapeForEachKeyPre210(key) {
124
+ return escapeReservedPunctuation(key);
125
+ }
126
+ /**
127
+ * Apply `escapeKey` to the content of every quoted instance key in `address`,
128
+ * then run the address-level substitution: `[` to `:`, `]` and `"` deleted.
129
+ */
130
+ function escapeAddressWith(address, escapeKey) {
131
+ // Only a quoted key carries content that needs escaping; a `count` index is
132
+ // digits, which every layer leaves alone.
133
+ const keysEscaped = address.replace(/\["((?:[^"\\]|\\.)*)"\]/g, (_all, key) => `["${escapeKey(key)}"]`);
134
+ return keysEscaped.replace(/\[/g, ":").replace(/\]/g, "").replace(/"/g, "");
135
+ }
136
+ /**
137
+ * The `tofu-address` value for a canonical terraform address: the current
138
+ * escaping, which is the only one anything ever writes.
139
+ */
140
+ export function escapeAddress(address) {
141
+ return escapeAddressWith(address, escapeForEachKey);
142
+ }
143
+ /**
144
+ * Every escaping a live marker could have been written under, current first:
145
+ * the current one, the pre-#210 one (the doubling alone), and the pre-#178
146
+ * one (no key escaping at all). Duplicates are collapsed, so the common
147
+ * address with no `for_each` key yields exactly one string.
148
+ */
149
+ export function escapeAddressVariants(address) {
150
+ const variants = [
151
+ escapeAddress(address),
152
+ escapeAddressWith(address, escapeForEachKeyPre210),
153
+ escapeAddressWith(address, (key) => key),
154
+ ];
155
+ return [...new Set(variants)];
156
+ }
157
+ /**
158
+ * Does a live resource's `tofu-address` value name this declared address?
159
+ * The declared side is escaped and the strings are compared, three escapings
160
+ * deep for the migration windows; the tag is never decoded.
161
+ */
162
+ export function addressMatches(declaredAddress, tagValue) {
163
+ return escapeAddressVariants(declaredAddress).includes(tagValue);
164
+ }
165
+ /**
166
+ * Concatenate `tofu-address` and whatever continuation tags follow it, which
167
+ * is the only sanctioned way to read a split address.
168
+ *
169
+ * A chain with a gap in it (`tofu-address-3` present while `tofu-address-2`
170
+ * is not) cannot be concatenated into anything and returns `undefined` rather
171
+ * than the address up to the gap.
172
+ */
173
+ export function joinMarkerAddress(tags) {
174
+ const head = tags[MARKER_TAG_ADDRESS];
175
+ if (head === undefined || head === "")
176
+ return undefined;
177
+ let address = head;
178
+ let ended = false;
179
+ for (const key of MARKER_TAG_ADDRESS_CONTINUATIONS) {
180
+ const chunk = tags[key];
181
+ if (chunk === undefined || chunk === "") {
182
+ ended = true;
183
+ continue;
184
+ }
185
+ if (ended)
186
+ return undefined; // a gap: the chain is malformed, not shorter
187
+ address += chunk;
188
+ }
189
+ return address;
190
+ }
191
+ /**
192
+ * The ownership verdict `live/MARKERS.md`'s "Ownership semantics" section
193
+ * defines, read off one live resource's tags. Three outcomes, and no fourth:
194
+ * a `tofu-estate` value is the entire ownership claim with no secondary
195
+ * check, neither key present is foreign, and an estate without a readable
196
+ * address is malformed rather than either of the two.
197
+ */
198
+ export function readMarker(tags) {
199
+ const estate = tags?.[MARKER_TAG_ESTATE];
200
+ if (estate === undefined || estate === "")
201
+ return { kind: "foreign" };
202
+ const address = joinMarkerAddress(tags);
203
+ if (address === undefined) {
204
+ return {
205
+ kind: "malformed",
206
+ estate,
207
+ detail: `carries ${MARKER_TAG_ESTATE}=${estate} but no readable ${MARKER_TAG_ADDRESS} (missing, empty, or a continuation chain with a gap in it)`,
208
+ };
209
+ }
210
+ const slot = tags[MARKER_TAG_SLOT];
211
+ return { kind: "owned", estate, address, ...(slot ? { slot } : {}) };
212
+ }