@cdktn/hcl2cdk 0.24.0-pre.45 → 0.24.0-pre.48

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 (89) hide show
  1. package/LICENSE +355 -0
  2. package/README.md +1 -1
  3. package/build/__tests__/expressions.test.js +10 -19
  4. package/build/__tests__/functions.test.js +8 -18
  5. package/build/__tests__/testHelpers.js +3 -2
  6. package/build/coerceType.js +11 -21
  7. package/build/dynamic-blocks.js +3 -3
  8. package/build/expressions.js +13 -22
  9. package/build/function-bindings/functions.generated.js +2 -2
  10. package/build/generation.js +24 -34
  11. package/build/index.js +15 -25
  12. package/build/iteration.js +7 -6
  13. package/build/jsii-rosetta-workarounds.js +6 -5
  14. package/build/partialCode.js +11 -20
  15. package/build/provider.js +4 -3
  16. package/build/references.js +6 -5
  17. package/build/schema.js +8 -18
  18. package/build/terraformSchema.js +4 -4
  19. package/build/utils.js +3 -3
  20. package/build/variables.js +12 -21
  21. package/package.json +20 -17
  22. package/package.sh +1 -1
  23. package/src/__tests__/coerceType.test.ts +207 -0
  24. package/src/__tests__/expressionToTs.test.ts +1167 -0
  25. package/src/__tests__/expressions.test.ts +541 -0
  26. package/src/__tests__/findExpressionType.test.ts +112 -0
  27. package/src/__tests__/functions.test.ts +768 -0
  28. package/src/__tests__/generation.test.ts +72 -0
  29. package/src/__tests__/jsii-rosetta-workarounds.test.ts +145 -0
  30. package/src/__tests__/partialCode.test.ts +432 -0
  31. package/src/__tests__/terraformSchema.test.ts +107 -0
  32. package/src/__tests__/testHelpers.ts +11 -0
  33. package/src/coerceType.ts +261 -0
  34. package/src/dynamic-blocks.ts +61 -0
  35. package/src/expressions.ts +968 -0
  36. package/src/function-bindings/functions.generated.ts +1139 -0
  37. package/src/function-bindings/functions.ts +104 -0
  38. package/src/generation.ts +1189 -0
  39. package/src/index.ts +584 -0
  40. package/src/iteration.ts +156 -0
  41. package/src/jsii-rosetta-workarounds.ts +145 -0
  42. package/src/partialCode.ts +132 -0
  43. package/src/provider.ts +60 -0
  44. package/src/references.ts +193 -0
  45. package/src/schema.ts +74 -0
  46. package/src/terraformSchema.ts +182 -0
  47. package/src/types.ts +58 -0
  48. package/src/utils.ts +19 -0
  49. package/src/variables.ts +214 -0
  50. package/test/__snapshots__/backends.test.ts.snap +70 -0
  51. package/test/__snapshots__/externals.test.ts.snap +37 -0
  52. package/test/__snapshots__/granular-imports.test.ts.snap +180 -0
  53. package/test/__snapshots__/imports.test.ts.snap +159 -0
  54. package/test/__snapshots__/iteration.test.ts.snap +532 -0
  55. package/test/__snapshots__/jsiiLanguage.test.ts.snap +347 -0
  56. package/test/__snapshots__/locals.test.ts.snap +55 -0
  57. package/test/__snapshots__/modules.test.ts.snap +127 -0
  58. package/test/__snapshots__/outputs.test.ts.snap +77 -0
  59. package/test/__snapshots__/partialCode.test.ts.snap +120 -0
  60. package/test/__snapshots__/provider.test.ts.snap +128 -0
  61. package/test/__snapshots__/references.test.ts.snap +376 -0
  62. package/test/__snapshots__/resource-meta-properties.test.ts.snap +342 -0
  63. package/test/__snapshots__/resources.test.ts.snap +613 -0
  64. package/test/__snapshots__/tfExpressions.test.ts.snap +537 -0
  65. package/test/__snapshots__/typeCoercion.test.ts.snap +253 -0
  66. package/test/__snapshots__/variables.test.ts.snap +150 -0
  67. package/test/backends.test.ts +75 -0
  68. package/test/convertProject.test.ts +257 -0
  69. package/test/externals.test.ts +35 -0
  70. package/test/globalSetup.ts +224 -0
  71. package/test/globalTeardown.ts +11 -0
  72. package/test/granular-imports.test.ts +161 -0
  73. package/test/hcl2cdk.test.ts +88 -0
  74. package/test/helpers/convert.ts +543 -0
  75. package/test/helpers/tmp.ts +25 -0
  76. package/test/imports.test.ts +141 -0
  77. package/test/iteration.test.ts +342 -0
  78. package/test/jsiiLanguage.test.ts +73 -0
  79. package/test/locals.test.ts +47 -0
  80. package/test/modules.test.ts +143 -0
  81. package/test/outputs.test.ts +69 -0
  82. package/test/partialCode.test.ts +25 -0
  83. package/test/provider.test.ts +106 -0
  84. package/test/references.test.ts +287 -0
  85. package/test/resource-meta-properties.test.ts +288 -0
  86. package/test/resources.test.ts +551 -0
  87. package/test/tfExpressions.test.ts +300 -0
  88. package/test/typeCoercion.test.ts +154 -0
  89. package/test/variables.test.ts +96 -0
@@ -0,0 +1,968 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import * as t from "@babel/types";
4
+ import template from "@babel/template";
5
+ import { camelCase, fileBugCommentText, logger } from "./utils";
6
+ import {
7
+ IteratorVariableReference,
8
+ ProgramScope,
9
+ ResourceScope,
10
+ } from "./types";
11
+ import { getReferencesInExpression, getExpressionAst } from "@cdktn/hcl2json";
12
+ import {
13
+ TFExpressionSyntaxTree as tex,
14
+ wrapTerraformExpression,
15
+ } from "@cdktn/hcl2json";
16
+ import { functionsMap } from "./function-bindings/functions";
17
+ import { coerceType, findExpressionType } from "./coerceType";
18
+ import { AttributeType } from "@cdktn/commons";
19
+ import { getTypeAtPath } from "./terraformSchema";
20
+ import { containsReference } from "./references";
21
+ import { variableName } from "./variables";
22
+
23
+ const tfBinaryOperatorsToCdktf = {
24
+ logicalOr: "or",
25
+ logicalAnd: "and",
26
+ greaterThan: "gt",
27
+ greaterThanOrEqual: "gte",
28
+ lessThan: "lt",
29
+ lessThanOrEqual: "lte",
30
+ equal: "eq",
31
+ notEqual: "neq",
32
+ add: "add",
33
+ subtract: "sub",
34
+ multiply: "mul",
35
+ divide: "div",
36
+ modulo: "mod",
37
+ };
38
+
39
+ const tfUnaryOperatorsToCdktf = {
40
+ logicalNot: "not",
41
+ negate: "negate",
42
+ };
43
+
44
+ type supportedBinaryOperators = keyof typeof tfBinaryOperatorsToCdktf;
45
+ type supportedUnaryOperators = keyof typeof tfUnaryOperatorsToCdktf;
46
+
47
+ function traversalPartsToString(
48
+ traversals: tex.TerraformTraversalPart[],
49
+ asSuffix = false,
50
+ ) {
51
+ let seed = "";
52
+ if (asSuffix && tex.isNameTraversalPart(traversals[0])) {
53
+ seed = ".";
54
+ }
55
+ return traversals.reduce((acc, part) => {
56
+ if (part.type === "nameTraversal") {
57
+ if (acc === seed) {
58
+ return `${acc}${part.segment}`;
59
+ }
60
+ return `${acc}.${part.segment}`;
61
+ }
62
+ return `${acc}[${part.segment}]`;
63
+ }, seed);
64
+ }
65
+
66
+ function canUseFqn(expression: tex.ExpressionType) {
67
+ if (!tex.isScopeTraversalExpression(expression)) {
68
+ return false;
69
+ }
70
+
71
+ const rootSegment = expression.meta.traversal[0].segment;
72
+
73
+ return !["var", "local"].includes(rootSegment);
74
+ }
75
+
76
+ function traversalToVariableName(
77
+ scope: ProgramScope,
78
+ node: tex.ExpressionType,
79
+ ) {
80
+ if (!tex.isScopeTraversalExpression(node)) {
81
+ logger.error(
82
+ `Unexpected expression type ${node.type} with value ${node.meta.value} passed to convert to a variable. ${fileBugCommentText}`,
83
+ );
84
+ return "";
85
+ }
86
+
87
+ const segments = node.meta.traversal;
88
+ if (segments.length === 1) {
89
+ return node.meta.fullAccessor;
90
+ }
91
+ const rootSegment = segments[0].segment;
92
+ const resource =
93
+ rootSegment === "data"
94
+ ? `${segments[0].segment}.${segments[1].segment}`
95
+ : rootSegment;
96
+ const name =
97
+ rootSegment === "data" ? segments[2].segment : segments[1].segment;
98
+
99
+ return variableName(scope, resource, name);
100
+ }
101
+
102
+ function expressionForSerialStringConcatenation(
103
+ scope: ResourceScope,
104
+ nodes: t.Expression[],
105
+ ) {
106
+ const reducedNodes = nodes.reduce((acc, node) => {
107
+ const prev = acc[acc.length - 1];
108
+ if (!prev) return [node];
109
+
110
+ if (t.isStringLiteral(prev) && t.isStringLiteral(node)) {
111
+ acc.pop();
112
+ acc.push(t.stringLiteral(prev.value + node.value));
113
+ return acc;
114
+ }
115
+
116
+ acc.push(node);
117
+ return acc;
118
+ }, [] as t.Expression[]);
119
+
120
+ return reducedNodes.reduce(
121
+ (acc: t.Expression | undefined, node: t.Expression) => {
122
+ if (!acc) {
123
+ return node;
124
+ }
125
+
126
+ // wrap access to dynamic blocks in Token.asString() as they return a Lazy
127
+ // for .key and .value which can't be concatenated in languages like Python
128
+ // because JSII currently has no support for the toString() method in
129
+ // languages other than TypeScript: https://github.com/aws/jsii/issues/380
130
+ // example: dynamic_iterator0.key / dynamic_iterator0.value
131
+ if (
132
+ t.isMemberExpression(node) &&
133
+ t.isIdentifier(node.object) &&
134
+ Object.values(scope.scopedVariables || {}).includes(node.object.name) &&
135
+ t.isIdentifier(node.property) &&
136
+ ["key", "value"].includes(node.property.name)
137
+ ) {
138
+ node = coerceType(scope, node, "dynamic", "string");
139
+ }
140
+
141
+ return t.binaryExpression("+", acc as t.Expression, node);
142
+ },
143
+ );
144
+ }
145
+
146
+ function getTfResourcePathFromNode(node: tex.ScopeTraversalExpression) {
147
+ const segments = node.meta.traversal;
148
+ let resource = segments[0].segment;
149
+ let result = [];
150
+ let attributes = [];
151
+
152
+ if (segments[0].segment === "data") {
153
+ result.push(segments[0].segment);
154
+ resource = segments[1].segment;
155
+ attributes = segments.slice(3); // we want to skip the variable name
156
+ } else {
157
+ attributes = segments.slice(2); // we want to skip the variable name
158
+ }
159
+
160
+ const [provider, ...resourceNameFragments] = resource.split("_");
161
+
162
+ // Hack: This happens in the case of `external` provider
163
+ // where the data source does not have a provider name prefix
164
+ if (resourceNameFragments.length === 0) {
165
+ resourceNameFragments.push(provider);
166
+ }
167
+
168
+ result.push(provider);
169
+ result.push(resourceNameFragments.join("_"));
170
+ result = [
171
+ ...result,
172
+ ...attributes.map((seg) => {
173
+ if (tex.isIndexTraversalPart(seg)) {
174
+ return `[${seg.segment}]`;
175
+ }
176
+ return seg.segment;
177
+ }),
178
+ ];
179
+
180
+ return result.join(".");
181
+ }
182
+
183
+ function convertLiteralValueExpressionToTs(
184
+ _scope: ResourceScope,
185
+ node: tex.LiteralValueExpression,
186
+ ) {
187
+ const literalType = node.meta.type;
188
+ if (literalType === "number") {
189
+ return t.numericLiteral(Number(node.meta.value));
190
+ }
191
+ if (literalType === "bool") {
192
+ return t.booleanLiteral(node.meta.value === "true" ? true : false);
193
+ }
194
+
195
+ return t.stringLiteral(node.meta.value);
196
+ }
197
+
198
+ function convertScopeTraversalExpressionToTs(
199
+ scope: ResourceScope,
200
+ node: tex.ScopeTraversalExpression,
201
+ ) {
202
+ const hasReference = containsReference(node);
203
+
204
+ const segments = node.meta.traversal;
205
+
206
+ if (segments[0].segment === "each" && scope.forEachIteratorName) {
207
+ return dynamicVariableToAst(scope, node, scope.forEachIteratorName);
208
+ }
209
+
210
+ if (segments[0].segment === "count" && scope.countIteratorName) {
211
+ return dynamicVariableToAst(scope, node, scope.countIteratorName, "count");
212
+ }
213
+
214
+ if (segments[0].segment === "self") {
215
+ scope.importables.push({
216
+ constructName: "TerraformSelf",
217
+ provider: "cdktn",
218
+ });
219
+
220
+ return t.callExpression(
221
+ t.memberExpression(t.identifier("TerraformSelf"), t.identifier("getAny")),
222
+
223
+ [t.stringLiteral(traversalPartsToString(segments.slice(1)))],
224
+ );
225
+ }
226
+
227
+ // setting.value, setting.value[1].id
228
+ const dynamicBlock = scope.scopedVariables?.[segments[0].segment];
229
+ if (dynamicBlock) {
230
+ if (dynamicBlock === "dynamic-block") {
231
+ return dynamicVariableToAst(
232
+ scope,
233
+ node,
234
+ dynamicBlock,
235
+ traversalPartsToString(segments),
236
+ );
237
+ }
238
+ return dynamicVariableToAst(scope, node, dynamicBlock, segments[0].segment);
239
+ }
240
+
241
+ // This may be a variable reference that we don't understand yet, so we wrap it in a template string
242
+ // for Terraform to handle
243
+ let varIdentifier: t.Expression = t.stringLiteral(
244
+ `\${${node.meta.fullAccessor}}`,
245
+ );
246
+
247
+ if (hasReference) {
248
+ varIdentifier = t.identifier(
249
+ camelCase(traversalToVariableName(scope, node)),
250
+ );
251
+ }
252
+
253
+ if (["var", "local"].includes(segments[0].segment)) {
254
+ const variableAccessor =
255
+ segments[0].segment === "var"
256
+ ? t.memberExpression(varIdentifier, t.identifier("value"))
257
+ : varIdentifier;
258
+
259
+ if (segments.length > 2) {
260
+ scope.importables.push({
261
+ constructName: "Fn",
262
+ provider: "cdktn",
263
+ });
264
+ const callee = t.memberExpression(
265
+ t.identifier("Fn"),
266
+ t.identifier("lookupNested"),
267
+ );
268
+ return t.callExpression(callee, [
269
+ variableAccessor,
270
+ t.arrayExpression(
271
+ segments.slice(2).map((s) => t.stringLiteral(s.segment)),
272
+ ),
273
+ ]);
274
+ }
275
+
276
+ return variableAccessor;
277
+ }
278
+
279
+ if (!hasReference || scope.withinOverrideExpression) {
280
+ return varIdentifier;
281
+ }
282
+
283
+ const rootSegment = segments[0].segment;
284
+ const attributeIndex = rootSegment === "data" ? 3 : 2;
285
+ const attributeSegments = segments.slice(attributeIndex);
286
+ const numericAccessorIndex = attributeSegments.findIndex((seg) =>
287
+ tex.isIndexTraversalPart(seg),
288
+ );
289
+ let minAccessorIndex = numericAccessorIndex;
290
+ let mapAccessorIndex = -1;
291
+ if (numericAccessorIndex === -1) {
292
+ // only do this if we have to, if we already have a
293
+ // numeric accessor, we don't have to do this additional work
294
+ const resourcePath = getTfResourcePathFromNode(node);
295
+ let usingSubPathType = false;
296
+ const parts = resourcePath.split(".").filter((p) => p !== "");
297
+ const minParts = attributeIndex; // we need to stop before data.aws.resource_name or aws.resource_name
298
+ const originalParts = parts.length;
299
+ let hasMapAccessor = false;
300
+ while (parts.length >= minParts) {
301
+ const type = getTypeAtPath(scope.providerSchema, parts.join("."));
302
+ if (type !== null) {
303
+ if (Array.isArray(type) && type[0] === "map" && usingSubPathType) {
304
+ hasMapAccessor = true;
305
+ break;
306
+ }
307
+ }
308
+ parts.pop();
309
+ usingSubPathType = true;
310
+ }
311
+
312
+ if (hasMapAccessor) {
313
+ mapAccessorIndex = originalParts - parts.length - 1;
314
+ minAccessorIndex = mapAccessorIndex;
315
+ }
316
+ }
317
+
318
+ const needsPropertyAccess = minAccessorIndex >= 0;
319
+
320
+ const refSegments = needsPropertyAccess
321
+ ? attributeSegments.slice(0, minAccessorIndex)
322
+ : attributeSegments;
323
+ const nonRefSegments = needsPropertyAccess
324
+ ? attributeSegments.slice(minAccessorIndex)
325
+ : [];
326
+
327
+ const ref = refSegments.reduce(
328
+ (acc: t.Expression, seg, index) =>
329
+ t.memberExpression(
330
+ acc,
331
+ t.identifier(
332
+ index === 0 && rootSegment === "module"
333
+ ? camelCase(seg.segment + "Output")
334
+ : camelCase(seg.segment),
335
+ ),
336
+ ),
337
+ varIdentifier,
338
+ );
339
+
340
+ if (nonRefSegments.length === 0) {
341
+ return ref;
342
+ }
343
+
344
+ scope.importables.push({
345
+ constructName: "Fn",
346
+ provider: "cdktn",
347
+ });
348
+ const callee = t.memberExpression(
349
+ t.identifier("Fn"),
350
+ t.identifier("lookupNested"),
351
+ );
352
+ return t.callExpression(callee, [
353
+ ref,
354
+ t.arrayExpression(nonRefSegments.map((s) => t.stringLiteral(s.segment))),
355
+ ]);
356
+ }
357
+
358
+ function convertUnaryOpExpressionToTs(
359
+ scope: ResourceScope,
360
+ node: tex.UnaryOpExpression,
361
+ ) {
362
+ const operand = convertTFExpressionAstToTs(
363
+ scope,
364
+ tex.getChildWithValue(node, node.meta.valueExpression)!,
365
+ );
366
+
367
+ let fnName = node.meta.operator;
368
+ if (tfUnaryOperatorsToCdktf[fnName as supportedUnaryOperators]) {
369
+ fnName = tfUnaryOperatorsToCdktf[fnName as supportedUnaryOperators];
370
+ } else {
371
+ throw new Error(`Cannot convert unknown operator ${node.meta.operator}`);
372
+ }
373
+
374
+ scope.importables.push({
375
+ constructName: "Op",
376
+ provider: "cdktn",
377
+ });
378
+
379
+ const fn = t.memberExpression(t.identifier("Op"), t.identifier(fnName));
380
+
381
+ return t.callExpression(fn, [operand]);
382
+ }
383
+
384
+ function convertBinaryOpExpressionToTs(
385
+ scope: ResourceScope,
386
+ node: tex.BinaryOpExpression,
387
+ ) {
388
+ const left = convertTFExpressionAstToTs(
389
+ scope,
390
+ tex.getChildWithValue(node, node.meta.lhsExpression)!,
391
+ );
392
+ const right = convertTFExpressionAstToTs(
393
+ scope,
394
+ tex.getChildWithValue(node, node.meta.rhsExpression)!,
395
+ );
396
+
397
+ let fnName = node.meta.operator;
398
+ if (tfBinaryOperatorsToCdktf[fnName as supportedBinaryOperators]) {
399
+ fnName = tfBinaryOperatorsToCdktf[fnName as supportedBinaryOperators];
400
+ } else {
401
+ throw new Error(`Cannot convert unknown operator ${node.meta.operator}`);
402
+ }
403
+
404
+ scope.importables.push({
405
+ constructName: "Op",
406
+ provider: "cdktn",
407
+ });
408
+
409
+ const fn = t.memberExpression(t.identifier("Op"), t.identifier(fnName));
410
+ return t.callExpression(fn, [left, right]);
411
+ }
412
+
413
+ function convertTemplateExpressionToTs(
414
+ scope: ResourceScope,
415
+ node: tex.TemplateExpression | tex.TemplateWrapExpression,
416
+ ) {
417
+ const parts = node.children.map((child) => ({
418
+ node: child,
419
+ expr: convertTFExpressionAstToTs(scope, child),
420
+ }));
421
+
422
+ const lastPart = parts[parts.length - 1];
423
+ if (t.isStringLiteral(lastPart.expr) && lastPart.expr.value === "\n") {
424
+ // This is a bit of a hack, but the trailing newline we add due to
425
+ // heredocs looks ugly and unnecessary in the generated code, so we
426
+ // try to remove it
427
+ parts.pop();
428
+ }
429
+
430
+ if (parts.length === 0) {
431
+ return t.stringLiteral(node.meta.value);
432
+ }
433
+
434
+ if (parts.length === 1) {
435
+ return parts[0].expr;
436
+ }
437
+
438
+ let isScopedTraversal = false;
439
+ const expressions: t.Expression[] = [];
440
+ for (const { node, expr } of parts) {
441
+ if (
442
+ tex.isScopeTraversalExpression(node) &&
443
+ !t.isStringLiteral(expr) &&
444
+ !t.isCallExpression(expr)
445
+ ) {
446
+ expressions.push(t.stringLiteral("${"));
447
+ isScopedTraversal = true;
448
+ } else if (
449
+ // we should ideally be doing type coercion more
450
+ // carefully here, because it may not always be needed
451
+ t.isCallExpression(expr)
452
+ ) {
453
+ scope.importables.push({
454
+ constructName: "Token",
455
+ provider: "cdktn",
456
+ });
457
+
458
+ expressions.push(
459
+ template.expression(`Token.asString(%%expr%%)`)({
460
+ expr,
461
+ }) as t.Expression,
462
+ );
463
+ continue;
464
+ } else {
465
+ if (isScopedTraversal) {
466
+ expressions.push(t.stringLiteral("}"));
467
+ isScopedTraversal = false;
468
+ }
469
+ }
470
+ expressions.push(expr);
471
+ }
472
+
473
+ if (isScopedTraversal) {
474
+ expressions.push(t.stringLiteral("}"));
475
+ }
476
+
477
+ return expressionForSerialStringConcatenation(scope, expressions);
478
+ }
479
+
480
+ function convertObjectExpressionToTs(
481
+ scope: ResourceScope,
482
+ node: tex.ObjectExpression,
483
+ ) {
484
+ return t.objectExpression(
485
+ Object.entries(node.meta.items)
486
+ .map(([key, value]) => {
487
+ const valueChild = tex.getChildWithValue(node, value);
488
+ if (!valueChild) {
489
+ logger.error(`Unable to value for object key '${key}': ${value}`);
490
+ return null;
491
+ }
492
+
493
+ return t.objectProperty(
494
+ t.identifier(key),
495
+ convertTFExpressionAstToTs(scope, valueChild),
496
+ );
497
+ })
498
+ .filter((s) => s !== null) as t.ObjectProperty[],
499
+ );
500
+ }
501
+
502
+ function convertFunctionCallExpressionToTs(
503
+ scope: ResourceScope,
504
+ node: tex.FunctionCallExpression,
505
+ ) {
506
+ const functionName = node.meta.name;
507
+ const mapping = functionsMap[functionName];
508
+
509
+ if (!mapping) {
510
+ logger.error(
511
+ `Unknown function ${functionName} encountered. ${fileBugCommentText}`,
512
+ );
513
+ const argumentExpressions = node.children.map((child) =>
514
+ convertTFExpressionAstToTs(scope, child),
515
+ );
516
+
517
+ return t.callExpression(t.identifier(functionName), argumentExpressions);
518
+ }
519
+
520
+ const transformedNode: tex.FunctionCallExpression = mapping.transformer
521
+ ? mapping.transformer(node)
522
+ : node;
523
+
524
+ const argumentExpressions = transformedNode.children.map((child) =>
525
+ convertTFExpressionAstToTs(scope, child),
526
+ );
527
+
528
+ scope.importables.push({
529
+ constructName: "Fn",
530
+ provider: "cdktn",
531
+ });
532
+
533
+ const callee = t.memberExpression(
534
+ t.identifier("Fn"),
535
+ t.identifier(mapping.name),
536
+ );
537
+
538
+ if (
539
+ mapping.parameters.length > 0 &&
540
+ mapping.parameters[mapping.parameters.length - 1].variadic
541
+ ) {
542
+ const lastParameterType =
543
+ mapping.parameters[mapping.parameters.length - 1].type;
544
+ const nonVariadicArguments = argumentExpressions.slice(
545
+ 0,
546
+ mapping.parameters.length - 1,
547
+ );
548
+
549
+ const fnCallArguments = [
550
+ ...nonVariadicArguments.map((argExpr, index) =>
551
+ coerceType(
552
+ scope,
553
+ argExpr,
554
+ findExpressionType(scope, argExpr),
555
+ mapping.parameters[index].type,
556
+ ),
557
+ ),
558
+
559
+ t.arrayExpression(
560
+ argumentExpressions
561
+ .slice(mapping.parameters.length - 1)
562
+ .map((argExpr) =>
563
+ coerceType(
564
+ scope,
565
+ argExpr,
566
+ findExpressionType(scope, argExpr),
567
+ lastParameterType,
568
+ ),
569
+ ),
570
+ ),
571
+ ];
572
+
573
+ return t.callExpression(callee, fnCallArguments);
574
+ }
575
+
576
+ return t.callExpression(
577
+ callee,
578
+ argumentExpressions.map((argExpr, index) =>
579
+ coerceType(
580
+ scope,
581
+ argExpr,
582
+ findExpressionType(scope, argExpr),
583
+ mapping.parameters[index].type,
584
+ ),
585
+ ),
586
+ );
587
+ }
588
+
589
+ function convertIndexExpressionToTs(
590
+ scope: ResourceScope,
591
+ node: tex.IndexExpression,
592
+ ) {
593
+ const collectionExpressionChild = tex.getChildWithValue(
594
+ node,
595
+ node.meta.collectionExpression,
596
+ );
597
+ const keyExpressionChild = tex.getChildWithValue(
598
+ node,
599
+ node.meta.keyExpression,
600
+ );
601
+
602
+ const collectionExpression = convertTFExpressionAstToTs(
603
+ scope,
604
+ collectionExpressionChild!,
605
+ );
606
+ const keyExpression = convertTFExpressionAstToTs(scope, keyExpressionChild!);
607
+
608
+ scope.importables.push({
609
+ constructName: "Fn",
610
+ provider: "cdktn",
611
+ });
612
+ const callee = t.memberExpression(
613
+ t.identifier("Fn"),
614
+ t.identifier("lookupNested"),
615
+ );
616
+ return t.callExpression(callee, [
617
+ collectionExpression,
618
+ t.arrayExpression([keyExpression]),
619
+ ]);
620
+ }
621
+
622
+ function convertSplatExpressionToTs(
623
+ scope: ResourceScope,
624
+ node: tex.SplatExpression,
625
+ ) {
626
+ const sourceExpressionChild = tex.getChildWithValue(
627
+ node,
628
+ node.meta.sourceExpression,
629
+ )!;
630
+ const sourceExpression = convertTFExpressionAstToTs(
631
+ scope,
632
+ sourceExpressionChild,
633
+ );
634
+
635
+ // We don't convert the relative expression because everything after the splat is going to be
636
+ // a string
637
+ const relativeExpression = node.meta.eachExpression.startsWith(
638
+ node.meta.anonSymbolExpression,
639
+ )
640
+ ? node.meta.eachExpression.slice(node.meta.anonSymbolExpression.length)
641
+ : node.meta.eachExpression;
642
+
643
+ const segments = relativeExpression.split(/\.|\[|\]/).filter((s) => s);
644
+ scope.importables.push({
645
+ constructName: "Fn",
646
+ provider: "cdktn",
647
+ });
648
+ const callee = t.memberExpression(
649
+ t.identifier("Fn"),
650
+ t.identifier("lookupNested"),
651
+ );
652
+
653
+ return t.callExpression(callee, [
654
+ sourceExpression,
655
+ t.arrayExpression([
656
+ // we don't need to use the anonSymbolExpression here because
657
+ // it only changes between .* and [*] which we don't care about
658
+ t.stringLiteral("*"),
659
+ ...segments.map(t.stringLiteral),
660
+ ]),
661
+ ]);
662
+ }
663
+
664
+ function convertConditionalExpressionToTs(
665
+ scope: ResourceScope,
666
+ node: tex.ConditionalExpression,
667
+ ) {
668
+ const conditionChild = tex.getChildWithValue(
669
+ node,
670
+ node.meta.conditionExpression,
671
+ )!;
672
+ let condition = convertTFExpressionAstToTs(scope, conditionChild);
673
+ if (t.isIdentifier(condition) && canUseFqn(conditionChild)) {
674
+ // We have a resource or data source here, which we would need to
675
+ // reference using fqn
676
+ condition = t.memberExpression(condition, t.identifier("fqn"));
677
+ }
678
+
679
+ const trueExpression = convertTFExpressionAstToTs(
680
+ scope,
681
+ tex.getChildWithValue(node, node.meta.trueExpression)!,
682
+ );
683
+
684
+ const falseExpression = convertTFExpressionAstToTs(
685
+ scope,
686
+ tex.getChildWithValue(node, node.meta.falseExpression)!,
687
+ );
688
+
689
+ scope.importables.push({
690
+ constructName: "conditional",
691
+ provider: "cdktn",
692
+ });
693
+
694
+ return t.callExpression(t.identifier("conditional"), [
695
+ condition,
696
+ trueExpression,
697
+ falseExpression,
698
+ ]);
699
+ }
700
+
701
+ function convertTupleExpressionToTs(
702
+ scope: ResourceScope,
703
+ node: tex.TupleExpression,
704
+ ) {
705
+ const expressions = node.children.map((child) =>
706
+ convertTFExpressionAstToTs(scope, child),
707
+ );
708
+
709
+ return t.arrayExpression(expressions);
710
+ }
711
+
712
+ function convertRelativeTraversalExpressionToTs(
713
+ scope: ResourceScope,
714
+ node: tex.RelativeTraversalExpression,
715
+ ) {
716
+ const segments = node.meta.traversal;
717
+
718
+ // The left hand side / source of a relative traversal is not a proper
719
+ // object / resource / data thing that is being referenced
720
+ const source = convertTFExpressionAstToTs(
721
+ scope,
722
+ tex.getChildWithValue(node, node.meta.sourceExpression)!,
723
+ );
724
+
725
+ scope.importables.push({
726
+ constructName: "Fn",
727
+ provider: "cdktn",
728
+ });
729
+ const callee = t.memberExpression(
730
+ t.identifier("Fn"),
731
+ t.identifier("lookupNested"),
732
+ );
733
+
734
+ return t.callExpression(callee, [
735
+ source,
736
+ t.arrayExpression(segments.map((s) => t.stringLiteral(s.segment))),
737
+ ]);
738
+ }
739
+
740
+ function convertForExpressionToTs(
741
+ scope: ResourceScope,
742
+ node: tex.ForExpression,
743
+ ) {
744
+ const collectionChild = tex.getChildWithValue(
745
+ node,
746
+ node.meta.collectionExpression,
747
+ )!;
748
+
749
+ let collectionExpression = convertTFExpressionAstToTs(scope, collectionChild);
750
+
751
+ if (t.isIdentifier(collectionExpression) && canUseFqn(collectionChild)) {
752
+ // We have a resource or data source here, which we would need to
753
+ // reference using fqn
754
+ collectionExpression = t.memberExpression(
755
+ collectionExpression,
756
+ t.identifier("fqn"),
757
+ );
758
+ }
759
+
760
+ const collectionRequiresWrapping = !t.isStringLiteral(collectionExpression);
761
+ const expressions = [];
762
+ const conditionBody = node.meta.keyVar
763
+ ? `${node.meta.keyVar}, ${node.meta.valVar}`
764
+ : node.meta.valVar;
765
+
766
+ const openBrace = node.meta.openRangeValue;
767
+ const closeBrace = node.meta.closeRangeValue;
768
+ const grouped = node.meta.groupedValue ? "..." : "";
769
+ const valueExpression = `${node.meta.valueExpression}${grouped}`;
770
+
771
+ const prefix = `\${${openBrace} for ${conditionBody} in `;
772
+ const keyValue = node.meta.keyExpression
773
+ ? ` : ${node.meta.keyExpression} => ${valueExpression}`
774
+ : ` : ${valueExpression}`;
775
+ const conditional = node.meta.conditionalExpression;
776
+ const suffix = `${keyValue}${
777
+ conditional ? ` if ${conditional}` : ""
778
+ }${closeBrace}}`;
779
+
780
+ expressions.push(t.stringLiteral(prefix));
781
+ if (collectionRequiresWrapping) {
782
+ expressions.push(t.stringLiteral("${"));
783
+ }
784
+ expressions.push(collectionExpression);
785
+ if (collectionRequiresWrapping) {
786
+ expressions.push(t.stringLiteral("}"));
787
+ }
788
+ expressions.push(t.stringLiteral(suffix));
789
+
790
+ return expressionForSerialStringConcatenation(scope, expressions);
791
+ }
792
+
793
+ function convertTFExpressionAstToTs(
794
+ scope: ResourceScope,
795
+ node: tex.ExpressionType,
796
+ ): t.Expression {
797
+ if (tex.isLiteralValueExpression(node)) {
798
+ return convertLiteralValueExpressionToTs(scope, node);
799
+ }
800
+
801
+ if (tex.isScopeTraversalExpression(node)) {
802
+ return convertScopeTraversalExpressionToTs(scope, node);
803
+ }
804
+
805
+ if (tex.isUnaryOpExpression(node)) {
806
+ return convertUnaryOpExpressionToTs(scope, node);
807
+ }
808
+
809
+ if (tex.isBinaryOpExpression(node)) {
810
+ return convertBinaryOpExpressionToTs(scope, node);
811
+ }
812
+
813
+ if (tex.isTemplateExpression(node) || tex.isTemplateWrapExpression(node)) {
814
+ return convertTemplateExpressionToTs(scope, node);
815
+ }
816
+
817
+ if (tex.isObjectExpression(node)) {
818
+ return convertObjectExpressionToTs(scope, node);
819
+ }
820
+
821
+ if (tex.isFunctionCallExpression(node)) {
822
+ return convertFunctionCallExpressionToTs(scope, node);
823
+ }
824
+
825
+ if (tex.isIndexExpression(node)) {
826
+ return convertIndexExpressionToTs(scope, node);
827
+ }
828
+
829
+ if (tex.isSplatExpression(node)) {
830
+ return convertSplatExpressionToTs(scope, node);
831
+ }
832
+
833
+ if (tex.isConditionalExpression(node)) {
834
+ return convertConditionalExpressionToTs(scope, node);
835
+ }
836
+
837
+ if (tex.isTupleExpression(node)) {
838
+ return convertTupleExpressionToTs(scope, node);
839
+ }
840
+
841
+ if (tex.isRelativeTraversalExpression(node)) {
842
+ return convertRelativeTraversalExpressionToTs(scope, node);
843
+ }
844
+
845
+ if (tex.isForExpression(node)) {
846
+ return convertForExpressionToTs(scope, node);
847
+ }
848
+
849
+ return t.stringLiteral("");
850
+ }
851
+
852
+ export async function expressionAst(
853
+ input: string,
854
+ ): Promise<tex.ExpressionType> {
855
+ const { wrap, wrapOffset } = wrapTerraformExpression(input);
856
+ const ast = await getExpressionAst("main.tf", wrap);
857
+
858
+ if (!ast) {
859
+ throw new Error(`Unable to parse terraform expression: ${input}`);
860
+ }
861
+
862
+ if (wrapOffset != 0 && tex.isTemplateWrapExpression(ast)) {
863
+ return ast.children[0];
864
+ }
865
+
866
+ return ast;
867
+ }
868
+
869
+ export async function convertTerraformExpressionToTs(
870
+ scope: ResourceScope,
871
+ input: string,
872
+ targetType: () => AttributeType,
873
+ ): Promise<t.Expression> {
874
+ logger.debug(`convertTerraformExpressionToTs(${input})`);
875
+ const tsExpression = convertTFExpressionAstToTs(
876
+ scope,
877
+ await expressionAst(input),
878
+ );
879
+
880
+ return coerceType(
881
+ scope,
882
+ tsExpression,
883
+ findExpressionType(scope, tsExpression),
884
+ targetType(),
885
+ );
886
+ }
887
+
888
+ export async function extractIteratorVariablesFromExpression(
889
+ input: string,
890
+ ): Promise<IteratorVariableReference[]> {
891
+ const possibleVariableSpots = await getReferencesInExpression(
892
+ "main.tf",
893
+ input,
894
+ );
895
+
896
+ return possibleVariableSpots
897
+ .filter((spot) => spot.value.startsWith("each."))
898
+ .map((spot) => ({
899
+ start: spot.startPosition,
900
+ end: spot.endPosition,
901
+ value: spot.value,
902
+ }));
903
+ }
904
+
905
+ export function dynamicVariableToAst(
906
+ scope: ProgramScope,
907
+ node: tex.ScopeTraversalExpression,
908
+ iteratorName: string,
909
+ block: string = "each",
910
+ ): t.Expression {
911
+ if (iteratorName === "dynamic-block") {
912
+ return expressionForSerialStringConcatenation(scope, [
913
+ t.stringLiteral("${"),
914
+ t.stringLiteral(block),
915
+ t.stringLiteral("}"),
916
+ ]);
917
+ }
918
+ if (node.meta.value === `${block}.key`) {
919
+ return t.memberExpression(t.identifier(iteratorName), t.identifier("key"));
920
+ }
921
+ if (node.meta.value === `${block}.value`) {
922
+ return t.memberExpression(
923
+ t.identifier(iteratorName),
924
+ t.identifier("value"),
925
+ );
926
+ }
927
+
928
+ if (block === "count" && node.meta.value === `${block}.index`) {
929
+ return t.memberExpression(
930
+ t.identifier(iteratorName),
931
+ t.identifier("index"),
932
+ );
933
+ }
934
+
935
+ const segments = node.meta.traversal;
936
+
937
+ if (
938
+ segments.length > 2 &&
939
+ segments[0].segment === block &&
940
+ segments[1].segment === "value"
941
+ ) {
942
+ const segmentsAfterEachValue = segments.slice(2);
943
+ scope.importables.push({
944
+ constructName: "Fn",
945
+ provider: "cdktn",
946
+ });
947
+ const callee = t.memberExpression(
948
+ t.identifier("Fn"),
949
+ t.identifier("lookupNested"),
950
+ );
951
+ return t.callExpression(callee, [
952
+ t.memberExpression(t.identifier(iteratorName), t.identifier("value")),
953
+ t.arrayExpression(
954
+ segmentsAfterEachValue.map((part) => {
955
+ if (part.type === "nameTraversal") {
956
+ return t.stringLiteral(part.segment);
957
+ } else {
958
+ return t.stringLiteral(`[${part.segment}]`);
959
+ }
960
+ }),
961
+ ),
962
+ ]);
963
+ }
964
+
965
+ throw new Error(
966
+ `Can not create AST for iterator variable of '${node.meta.value}'`,
967
+ );
968
+ }