@cdktn/hcl2cdk 0.24.0-pre.43 → 0.24.0-pre.47

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,156 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import { DirectedGraph } from "graphology";
4
+ import { providers as telemetryAllowedProviders } from "./telemetryAllowList.json";
5
+ import { ProgramScope } from "./types";
6
+ import { Import } from "./schema";
7
+
8
+ // locals, variables, and outputs are global key value maps
9
+ export function forEachGlobal<T, R>(
10
+ scope: ProgramScope,
11
+ prefix: string,
12
+ record: Record<string, T> | undefined,
13
+ iterator: (
14
+ scope: ProgramScope,
15
+ key: string,
16
+ id: string,
17
+ value: T,
18
+ graph: DirectedGraph,
19
+ ) => Promise<R>,
20
+ ): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
21
+ return Object.entries(record || {}).reduce((carry, [key, item]) => {
22
+ const id = `${prefix}.${key}`;
23
+ return {
24
+ ...carry,
25
+ [id]: {
26
+ code: async (graph: DirectedGraph) =>
27
+ await iterator(scope, key, id, item, graph),
28
+ value: item,
29
+ },
30
+ };
31
+ }, {});
32
+ }
33
+
34
+ export function forEachImport<R>(
35
+ scope: ProgramScope,
36
+ prefix: string,
37
+ record: Import[] | undefined,
38
+ iterator: (
39
+ scope: ProgramScope,
40
+ id: string,
41
+ value: Import,
42
+ graph: DirectedGraph,
43
+ ) => Promise<R>,
44
+ ): Record<
45
+ string,
46
+ { code: (graph: DirectedGraph) => Promise<R>; value: Import }
47
+ > {
48
+ return (record || []).reduce((carry, item) => {
49
+ const target =
50
+ item.to.startsWith("${") && item.to.endsWith("}")
51
+ ? item.to.substring(2, item.to.length - 1)
52
+ : item.to;
53
+
54
+ const id = `${prefix}.${target}`;
55
+ return {
56
+ ...carry,
57
+ [id]: {
58
+ code: async (graph: DirectedGraph) =>
59
+ await iterator(scope, id, item, graph),
60
+ value: item,
61
+ },
62
+ };
63
+ }, {});
64
+ }
65
+
66
+ export function forEachProvider<T extends { alias?: string }, R>(
67
+ scope: ProgramScope,
68
+ record: Record<string, T[]> | undefined,
69
+ iterator: (
70
+ scope: ProgramScope,
71
+ key: string,
72
+ id: string,
73
+ value: T,
74
+ graph: DirectedGraph,
75
+ ) => Promise<R>,
76
+ ): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
77
+ return Object.entries(record || {}).reduce((carry, [key, items]) => {
78
+ return {
79
+ ...carry,
80
+ ...items.reduce((innerCarry, item: T) => {
81
+ const id = item.alias ? `${key}.${item.alias}` : `${key}`;
82
+ return {
83
+ ...innerCarry,
84
+ [id]: {
85
+ code: async (graph: DirectedGraph) =>
86
+ await iterator(scope, key, id, item, graph),
87
+ value: item,
88
+ },
89
+ };
90
+ }, {}),
91
+ };
92
+ }, {});
93
+ }
94
+
95
+ // data and resource are namespaced key value maps
96
+ export function forEachNamespaced<T, R>(
97
+ scope: ProgramScope,
98
+ record: Record<string, Record<string, T>> | undefined,
99
+ iterator: (
100
+ scope: ProgramScope,
101
+ type: string,
102
+ key: string,
103
+ id: string,
104
+ value: T,
105
+ graph: DirectedGraph,
106
+ ) => Promise<R>,
107
+ prefix?: string,
108
+ ): Record<string, { code: (graph: DirectedGraph) => Promise<R>; value: T }> {
109
+ return Object.entries(record || {}).reduce(
110
+ (outerCarry, [type, items]) => ({
111
+ ...outerCarry,
112
+ ...Object.entries(items).reduce(
113
+ (innerCarry, [key, item]) => {
114
+ const prefixedType = prefix ? `${prefix}.${type}` : type;
115
+ const id = prefix ? `${prefix}.${type}.${key}` : `${type}.${key}`;
116
+ return {
117
+ ...innerCarry,
118
+ [id]: {
119
+ code: async (graph: DirectedGraph) =>
120
+ await iterator(scope, prefixedType, key, id, item, graph),
121
+ value: item,
122
+ },
123
+ };
124
+ },
125
+ {} as Record<
126
+ string,
127
+ { code: (graph: DirectedGraph) => Promise<R>; value: T }
128
+ >,
129
+ ),
130
+ }),
131
+ {} as Record<
132
+ string,
133
+ { code: (graph: DirectedGraph) => Promise<R>; value: T }
134
+ >,
135
+ );
136
+ }
137
+
138
+ export function resourceStats(obj: Record<string, Record<string, unknown>>) {
139
+ return Object.entries(obj).reduce(
140
+ (carry, [key, value]) => {
141
+ const [provider, ...resourceParts] = key.split("_");
142
+ const shouldBeTracked = telemetryAllowedProviders.includes(provider);
143
+ const providerKey = shouldBeTracked ? provider : "other";
144
+ const resourceName = shouldBeTracked ? resourceParts.join("_") : "other";
145
+
146
+ return {
147
+ ...carry,
148
+ [providerKey]: {
149
+ ...(carry[providerKey] || {}),
150
+ [resourceName]: Object.keys(value).length,
151
+ },
152
+ };
153
+ },
154
+ {} as Record<string, Record<string, number>>,
155
+ );
156
+ }
@@ -0,0 +1,145 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+
4
+ export function replacePythonImports(code: string) {
5
+ return code
6
+ .split("\n")
7
+ .map((line) => {
8
+ // Replace from-import lines with lib
9
+ const fromImportLibRegex =
10
+ /from \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) import/;
11
+ if (fromImportLibRegex.test(line)) {
12
+ return line.replace(fromImportLibRegex, "from imports.$1.$2 import");
13
+ }
14
+
15
+ // Replace import lines with lib
16
+ const importLibRegex =
17
+ /import \.\.\.gen\.providers\.([^.]+)(?:\.lib)?\.(.*) as (.*)/;
18
+ if (importLibRegex.test(line)) {
19
+ return line.replace(importLibRegex, "import imports.$1.$2 as $3");
20
+ }
21
+
22
+ // Replace from-import lines
23
+ if (line.startsWith("from ...gen.providers.")) {
24
+ return line.replace("from ...gen.providers.", "from imports.");
25
+ }
26
+ // Replace import lines
27
+ if (line.startsWith("import ...gen.providers.")) {
28
+ return line.replace("import ...gen.providers.", "import imports.");
29
+ }
30
+
31
+ // Replace modules
32
+ if (line.startsWith("import ...gen.modules.")) {
33
+ return line.replace("import ...gen.modules.", "import imports.");
34
+ }
35
+
36
+ return line;
37
+ })
38
+ .join("\n");
39
+ }
40
+
41
+ export function replaceJavaImports(code: string) {
42
+ return code
43
+ .split("\n")
44
+ .map((line) => {
45
+ // Replace using lines with lib and precices import
46
+ const importWithLib =
47
+ /import gen\.providers\.([^.]+)(?:\.lib)?\.([^.]+)\.(.*);/;
48
+ const matchWithLib = line.match(importWithLib);
49
+ if (matchWithLib) {
50
+ const [, provider, resource, className] = matchWithLib;
51
+ return `import imports.${provider}.${resource}.${className};`;
52
+ }
53
+
54
+ // Replace using lines
55
+ const importWithoutLib = /import gen\.providers\.([^.]+)\.([^.]+)\.\*;/;
56
+ const matchWithoutLib = line.match(importWithoutLib);
57
+ if (matchWithoutLib) {
58
+ const [, provider, resource] = matchWithoutLib;
59
+ return `import imports.${provider}.${resource}.*;`;
60
+ }
61
+
62
+ // Replace using lines
63
+ const importModules = /import gen\.modules\.(.+)\.\*;/;
64
+ const importModulesMatch = line.match(importModules);
65
+ if (importModulesMatch) {
66
+ const [, module] = importModulesMatch;
67
+ return `import imports.${module}.*;`;
68
+ }
69
+
70
+ return line;
71
+ })
72
+ .join("\n");
73
+ }
74
+
75
+ export function replaceCsharpImports(code: string) {
76
+ return code
77
+ .split("\n")
78
+ .map((line) => {
79
+ // Replace using lines with lib
80
+ const fromImportLibRegex =
81
+ /using Gen\.Providers\.([^.]*)(?:\.Lib)?\.(.*);/;
82
+ const match = line.match(fromImportLibRegex);
83
+ if (match) {
84
+ const [, provider, resource] = match;
85
+ return `using ${lowercaseFirstChar(provider)}.${resource};`;
86
+ }
87
+
88
+ // Replace using lines
89
+ if (line.startsWith("using Gen.Providers.")) {
90
+ const importLine = line.replace("using Gen.Providers.", "");
91
+
92
+ return `using ${importLine
93
+ .substring(0, 1)
94
+ .toLocaleLowerCase()}${importLine.substring(1)}`;
95
+ }
96
+
97
+ if (line.startsWith("using Gen.Modules.")) {
98
+ const importLine = line.replace("using Gen.Modules.", "");
99
+
100
+ return `using ${importLine}`;
101
+ }
102
+
103
+ return line;
104
+ })
105
+ .join("\n");
106
+ }
107
+
108
+ export function replaceGoImports(code: string) {
109
+ return code
110
+ .split("\n")
111
+ .map((line) => {
112
+ // Replace using lines with lib
113
+ const fromImportLibRegex =
114
+ /import "github.com\/aws-samples\/dummy\/gen\/providers\/([^/]*)(?:\/lib)?\/(.*)"/;
115
+ const matchLib = line.match(fromImportLibRegex);
116
+ if (matchLib) {
117
+ const [, provider, resource] = matchLib;
118
+ return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`;
119
+ }
120
+
121
+ // Replace using lines
122
+ const fromImportRegex =
123
+ /import "github.com\/aws-samples\/dummy\/gen\/providers\/([^/]+)\/(.*)"/;
124
+ const match = line.match(fromImportRegex);
125
+ if (match) {
126
+ const [, provider, resource] = match;
127
+ return `import "cdk.tf/go/stack/generated/${provider}/${resource}"`;
128
+ }
129
+
130
+ const importModulesRegex =
131
+ /import (.*) "github.com\/aws-samples\/dummy\/gen\/modules\/(.*)"/;
132
+ const matchModules = line.match(importModulesRegex);
133
+ if (matchModules) {
134
+ const [, name, module] = matchModules;
135
+ return `import ${name} "cdk.tf/go/stack/generated/${module}"`;
136
+ }
137
+
138
+ return line;
139
+ })
140
+ .join("\n");
141
+ }
142
+
143
+ function lowercaseFirstChar(str: string) {
144
+ return str.substring(0, 1).toLocaleLowerCase() + str.substring(1);
145
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Copyright (c) HashiCorp, Inc.
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ */
5
+
6
+ import { getTypeAtPath } from "./terraformSchema";
7
+ import { ResourceScope, TerraformResourceBlock } from "./types";
8
+ import * as t from "@babel/types";
9
+ import { camelCase } from "./utils";
10
+
11
+ function getConfigFieldName(
12
+ topLevelConfig: Record<string, unknown>,
13
+ name: string,
14
+ ) {
15
+ const sanitizedName = camelCase(name);
16
+ return deduplicateName(Object.keys(topLevelConfig), sanitizedName);
17
+ }
18
+
19
+ function deduplicateName(existingNames: string[], name: string) {
20
+ let newName = name;
21
+ let i = 1;
22
+ while (existingNames.includes(newName)) {
23
+ newName = `${name}${i}`;
24
+ i++;
25
+ }
26
+ return newName;
27
+ }
28
+
29
+ export function fillWithConfigAccessors(
30
+ scope: ResourceScope,
31
+ config: TerraformResourceBlock,
32
+ path: string,
33
+ ): any {
34
+ if (Array.isArray(config)) {
35
+ return config.map((c) => fillWithConfigAccessors(scope, c, `${path}.[]`));
36
+ }
37
+
38
+ if (typeof config === "object" && config !== null) {
39
+ const mutated = Object.entries(config).reduce(
40
+ (acc, [key, value]) => ({
41
+ ...acc,
42
+ [key]: fillWithConfigAccessors(scope, value, `${path}.${key}`),
43
+ }),
44
+ {} as Record<string, TerraformResourceBlock>,
45
+ );
46
+
47
+ // Get type of this part of the config
48
+ const attributeType = getTypeAtPath(scope.providerSchema, path);
49
+ const requiredAttributes = getRequiredAttributes(attributeType);
50
+
51
+ // Add accessors for all required attributes that are missing
52
+ requiredAttributes.forEach((key) => {
53
+ const value = mutated[key];
54
+ const isNotDirectlyAccessible = value === undefined;
55
+ const isReplacedByAst =
56
+ (t.isNode(mutated) && t.isExpression(mutated)) ||
57
+ t.isExpression(value as any);
58
+ const isEmptyArray = Array.isArray(value) && value.length === 0;
59
+
60
+ // If this was already replaced by an AST node, we don't need to do anything
61
+ // We assume all fields are filled in by the AST
62
+ if (isReplacedByAst) {
63
+ return;
64
+ }
65
+
66
+ if (isNotDirectlyAccessible || isEmptyArray) {
67
+ const fieldName = getConfigFieldName(scope.topLevelConfig, key);
68
+ mutated[key] = t.memberExpression(
69
+ t.identifier("config"),
70
+ t.identifier(fieldName),
71
+ );
72
+ scope.topLevelConfig[fieldName] = `${path}.${key}`;
73
+ }
74
+ });
75
+
76
+ return mutated;
77
+ } else {
78
+ return config;
79
+ }
80
+ }
81
+
82
+ type Key = string;
83
+ export function getRequiredAttributes(
84
+ attributeType: ReturnType<typeof getTypeAtPath>,
85
+ ): Key[] {
86
+ if (!attributeType) {
87
+ return [];
88
+ }
89
+ if (
90
+ typeof attributeType !== "object" ||
91
+ Array.isArray(attributeType) ||
92
+ attributeType === null ||
93
+ attributeType === undefined ||
94
+ !("block" in attributeType)
95
+ ) {
96
+ return [];
97
+ }
98
+
99
+ const requiredAttributes = Object.entries(
100
+ attributeType.block.attributes || {},
101
+ ).reduce(
102
+ (acc, [key, value]) => (value.required ? [...acc, key] : acc),
103
+ [] as string[],
104
+ );
105
+
106
+ // Logic taken from (and should be shared with) provider generator resource parser: attributeForBlockType
107
+ const requiredBlockTypes = Object.entries(
108
+ attributeType.block.block_types || {},
109
+ ).reduce((acc, [key, value]) => {
110
+ if (
111
+ value.nesting_mode === "single" &&
112
+ !Object.values(value.block.attributes || {}).some((x) => !x.required)
113
+ ) {
114
+ return [...acc, key];
115
+ }
116
+ if (value.nesting_mode === "map") {
117
+ return acc;
118
+ }
119
+
120
+ if (
121
+ (value.nesting_mode === "list" || value.nesting_mode === "set") &&
122
+ value.min_items === undefined
123
+ ? false
124
+ : (value as any).min_items > 0
125
+ ) {
126
+ return [...acc, key];
127
+ }
128
+
129
+ return acc;
130
+ }, [] as string[]);
131
+ return [...requiredAttributes, ...requiredBlockTypes].sort();
132
+ }
@@ -0,0 +1,60 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import * as z from "zod";
4
+ import { ProviderSchema, BlockType, Attribute } from "@cdktn/commons";
5
+ import { schema } from "./schema";
6
+
7
+ export { BlockType, Attribute };
8
+
9
+ export function getFullProviderName(
10
+ schema: ProviderSchema,
11
+ providerName: string,
12
+ ) {
13
+ return Object.keys(schema.provider_schemas || {}).find((name) =>
14
+ name.endsWith(providerName),
15
+ );
16
+ }
17
+
18
+ type Plan = z.infer<typeof schema>;
19
+ export function getProviderRequirements(plan: Plan) {
20
+ // In Terraform one can implicitly define the provider by using resources of that type
21
+ const explicitProviders = Object.keys(plan.provider || {});
22
+ const implicitProviders = Object.keys({ ...plan.resource, ...plan.data })
23
+ .filter((type) => type !== "terraform_remote_state")
24
+ .filter((type) => type !== "terraform_data")
25
+ .map((type) => type.split("_")[0]);
26
+
27
+ const providerRequirements = Array.from(
28
+ new Set([...explicitProviders, ...implicitProviders]),
29
+ ).reduce(
30
+ (carry, req) => ({ ...carry, [req]: "*" }),
31
+ {} as Record<string, string>,
32
+ );
33
+
34
+ plan.terraform?.forEach(({ required_providers }) =>
35
+ (required_providers || []).forEach((providerBlock) =>
36
+ Object.entries(providerBlock).forEach(([key, value]) => {
37
+ let name, version;
38
+ if (typeof value === "string") {
39
+ name = key;
40
+ version = value;
41
+ } else {
42
+ name = value.source;
43
+ version = value.version;
44
+ }
45
+
46
+ if (!name) {
47
+ return;
48
+ }
49
+ // implicitly only the last part of the path is used (e.g. docker for kreuzwerker/docker)
50
+ const parts = name.split("/");
51
+ if (parts.length > 1) {
52
+ delete providerRequirements[parts.pop() || ""];
53
+ }
54
+ providerRequirements[name] = version || "*";
55
+ }),
56
+ ),
57
+ );
58
+
59
+ return providerRequirements;
60
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Copyright (c) HashiCorp, Inc.
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ */
5
+
6
+ import {
7
+ getReferencesInExpression,
8
+ TFExpressionSyntaxTree as tex,
9
+ } from "@cdktn/hcl2json";
10
+ import { logger } from "./utils";
11
+ import { ProgramScope, Reference, TerraformResourceBlock } from "./types";
12
+ import { variableName } from "./variables";
13
+
14
+ export function referenceToVariableName(
15
+ scope: ProgramScope,
16
+ ref: Reference,
17
+ ): string {
18
+ const parts = ref.referencee.id.split(".");
19
+ const resource = parts[0] === "data" ? `${parts[0]}.${parts[1]}` : parts[0];
20
+ const name = parts[0] === "data" ? parts[2] : parts[1];
21
+ return variableName(scope, resource, name);
22
+ }
23
+
24
+ export function containsReference(expression: tex.ExpressionType) {
25
+ if (!tex.isScopeTraversalExpression(expression)) {
26
+ return false;
27
+ }
28
+
29
+ const segments = expression.meta.traversal;
30
+ const rootSegment = segments[0].segment;
31
+ const fullAccessor = expression.meta.fullAccessor;
32
+
33
+ if (
34
+ rootSegment === "count" || // count variable
35
+ rootSegment === "each" || // each variable
36
+ // https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info
37
+ fullAccessor.startsWith("path.module") ||
38
+ fullAccessor.startsWith("path.root") ||
39
+ fullAccessor.startsWith("path.cwd") ||
40
+ fullAccessor.startsWith("terraform.workspace") ||
41
+ fullAccessor.startsWith("self.") // block local value
42
+ ) {
43
+ logger.debug(`skipping ${fullAccessor}`);
44
+ return false;
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ export async function extractReferencesFromExpression(
51
+ input: string,
52
+ nodeIds: readonly string[],
53
+ scopedIds: readonly string[] = [], // dynamics introduce new scoped variables that are not the globally accessible ids
54
+ ): Promise<Reference[]> {
55
+ logger.debug(`extractReferencesFromExpression(${input})`);
56
+ const possibleVariableSpots = await getReferencesInExpression(
57
+ "main.tf",
58
+ input,
59
+ );
60
+
61
+ logger.debug(
62
+ `found possible variable spots: ${JSON.stringify(possibleVariableSpots)}`,
63
+ );
64
+
65
+ return possibleVariableSpots.reduce((carry, spot) => {
66
+ const { value, startPosition, endPosition } = spot;
67
+ // no reference
68
+ if (
69
+ !value.includes(".") || // just a literal
70
+ value.startsWith(".") || // dangling property access
71
+ value.endsWith("...") || // spread (likely in for loop)
72
+ value.startsWith("count.") || // count variable
73
+ value.startsWith("each.") || // each variable
74
+ // https://www.terraform.io/docs/language/expressions/references.html#filesystem-and-workspace-info
75
+ value.startsWith("path.module") ||
76
+ value.startsWith("path.root") ||
77
+ value.startsWith("path.cwd") ||
78
+ value.startsWith("terraform.workspace") ||
79
+ value.startsWith("self.") // block local value
80
+ ) {
81
+ logger.debug(`skipping ${value}`);
82
+ return carry;
83
+ }
84
+
85
+ const referenceParts = value.split(".");
86
+
87
+ logger.debug(
88
+ `Searching for node id '${value}' in ${JSON.stringify(nodeIds)}`,
89
+ );
90
+ const corespondingNodeId = [...nodeIds, ...scopedIds].find((id) => {
91
+ const parts = id.split(".");
92
+ const matchesFirst = parts[0] === referenceParts[0];
93
+ const matchesFirstTwo =
94
+ matchesFirst &&
95
+ (parts[1] === referenceParts[1] || referenceParts.length === 1);
96
+
97
+ return (
98
+ matchesFirstTwo &&
99
+ (parts[0] === "data" ? parts[2] === referenceParts[2] : true)
100
+ );
101
+ });
102
+
103
+ if (!corespondingNodeId) {
104
+ // This is most likely a false positive, so we just ignore it
105
+ // We include the log below to help debugging
106
+ logger.error(
107
+ `Found a reference that is unknown: ${input} has reference "${value}". The id was not found in ${JSON.stringify(
108
+ nodeIds,
109
+ )} with temporary values ${JSON.stringify(scopedIds)}.`,
110
+ );
111
+ return carry;
112
+ }
113
+
114
+ if (scopedIds.includes(corespondingNodeId)) {
115
+ logger.debug(`skipping '${value}' since it's a scoped variable`);
116
+ return carry;
117
+ }
118
+ logger.debug(`Found node id '${corespondingNodeId}'`);
119
+
120
+ const spotParts = value.split(".");
121
+ let isThereANumericAccessor = false;
122
+ const referenceSpotParts = spotParts.filter((part) => {
123
+ if (!Number.isNaN(parseInt(part, 10))) {
124
+ isThereANumericAccessor = true;
125
+ return false;
126
+ }
127
+
128
+ return !isThereANumericAccessor;
129
+ });
130
+ const fullReference = isThereANumericAccessor
131
+ ? referenceSpotParts.slice(0, 2).join(".")
132
+ : value;
133
+
134
+ const isVariable = value.startsWith("var.");
135
+ const useFqn =
136
+ // Can not use FQN on vars
137
+ !isVariable &&
138
+ // Can not use FQN on locals
139
+ !value.startsWith("local.") &&
140
+ // If the following character is
141
+ (input.substr(endPosition + 1, 1) === "*" || // a * (splat) we need to use the FQN
142
+ input.substr(endPosition, 1) === "[" || // a property access
143
+ isThereANumericAccessor); // a numeric access
144
+
145
+ const ref: Reference = {
146
+ start: startPosition,
147
+ end: endPosition,
148
+ referencee: {
149
+ id: corespondingNodeId,
150
+ full: fullReference,
151
+ },
152
+ useFqn,
153
+ isVariable,
154
+ };
155
+ logger.debug(`Found reference ${JSON.stringify(ref)}`);
156
+ return [...carry, ref];
157
+ }, [] as Reference[]);
158
+ }
159
+
160
+ export async function findUsedReferences(
161
+ nodeIds: string[],
162
+ item: TerraformResourceBlock,
163
+ ): Promise<Reference[]> {
164
+ logger.debug(`findUsedReferences(${nodeIds}, ${item})`);
165
+ if (typeof item === "string") {
166
+ return await extractReferencesFromExpression(item, nodeIds, []);
167
+ }
168
+
169
+ if (typeof item !== "object" || item === null || item === undefined) {
170
+ return [];
171
+ }
172
+
173
+ if (Array.isArray(item)) {
174
+ return (
175
+ await Promise.all(item.map((i) => findUsedReferences(nodeIds, i)))
176
+ ).flat();
177
+ }
178
+
179
+ if (item && "dynamic" in item) {
180
+ const dyn = (item as any)["dynamic"];
181
+ const { for_each: _for_each, ...others } = dyn;
182
+ const dynamicRef = Object.keys(others)[0];
183
+ return await findUsedReferences([...nodeIds, dynamicRef], dyn);
184
+ }
185
+
186
+ return (
187
+ await Promise.all(
188
+ Object.values(item as Record<string, any>).map((i) =>
189
+ findUsedReferences(nodeIds, i),
190
+ ),
191
+ )
192
+ ).flat();
193
+ }