@intentius/chant-lexicon-terraform 0.101.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
  2. package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
  3. package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
  4. package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
  5. package/dist/change-set.d.ts +67 -0
  6. package/dist/change-set.d.ts.map +1 -0
  7. package/dist/changed-roots.d.ts +30 -0
  8. package/dist/changed-roots.d.ts.map +1 -0
  9. package/dist/codegen/package.d.ts +1 -1
  10. package/dist/commands.d.ts +26 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/components/capability-plugin.d.ts +54 -0
  13. package/dist/components/capability-plugin.d.ts.map +1 -0
  14. package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
  15. package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
  16. package/dist/config.d.ts +3 -3
  17. package/dist/hcl/descend.d.ts +1 -1
  18. package/dist/hcl/descend.d.ts.map +1 -1
  19. package/dist/hcl/edges.d.ts +2 -2
  20. package/dist/hcl/edges.d.ts.map +1 -1
  21. package/dist/hcl/jsonencode.d.ts +106 -0
  22. package/dist/hcl/jsonencode.d.ts.map +1 -0
  23. package/dist/hcl/marker.d.ts.map +1 -1
  24. package/dist/hcl/parse.d.ts +29 -1
  25. package/dist/hcl/parse.d.ts.map +1 -1
  26. package/dist/hcl/roots.d.ts +1 -1
  27. package/dist/hcl/roots.d.ts.map +1 -1
  28. package/dist/hcl/suppressions.d.ts.map +1 -1
  29. package/dist/index.d.ts +4 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/integrity.json +22 -4
  32. package/dist/lint/audit-catalog.d.ts.map +1 -1
  33. package/dist/lint/audit-lineage.d.ts +1 -1
  34. package/dist/lint/audit-lineage.d.ts.map +1 -1
  35. package/dist/lint/post-synth/flat-attributes.d.ts +69 -0
  36. package/dist/lint/post-synth/flat-attributes.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/module-source.d.ts +27 -1
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -1
  40. package/dist/lint/post-synth/terragrunt.d.ts +37 -0
  41. package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf002.d.ts +3 -1
  43. package/dist/lint/post-synth/tf002.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf030.d.ts +51 -0
  45. package/dist/lint/post-synth/tf030.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf031.d.ts +55 -0
  47. package/dist/lint/post-synth/tf031.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf032.d.ts +71 -0
  49. package/dist/lint/post-synth/tf032.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf033.d.ts +50 -0
  51. package/dist/lint/post-synth/tf033.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf034.d.ts +25 -0
  53. package/dist/lint/post-synth/tf034.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf035.d.ts +33 -0
  55. package/dist/lint/post-synth/tf035.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf036.d.ts +33 -0
  57. package/dist/lint/post-synth/tf036.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf037.d.ts +31 -0
  59. package/dist/lint/post-synth/tf037.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf038.d.ts +22 -0
  61. package/dist/lint/post-synth/tf038.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf039.d.ts +23 -0
  63. package/dist/lint/post-synth/tf039.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf040.d.ts +30 -0
  65. package/dist/lint/post-synth/tf040.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf041.d.ts +20 -0
  67. package/dist/lint/post-synth/tf041.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf042.d.ts +18 -0
  69. package/dist/lint/post-synth/tf042.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf043.d.ts +17 -0
  71. package/dist/lint/post-synth/tf043.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf044.d.ts +17 -0
  73. package/dist/lint/post-synth/tf044.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf045.d.ts +22 -0
  75. package/dist/lint/post-synth/tf045.d.ts.map +1 -0
  76. package/dist/manifest.json +1 -1
  77. package/dist/okf/index.md +16 -0
  78. package/dist/okf/rules/TF030.md +11 -0
  79. package/dist/okf/rules/TF031.md +11 -0
  80. package/dist/okf/rules/TF032.md +11 -0
  81. package/dist/okf/rules/TF033.md +11 -0
  82. package/dist/okf/rules/TF034.md +11 -0
  83. package/dist/okf/rules/TF035.md +11 -0
  84. package/dist/okf/rules/TF036.md +11 -0
  85. package/dist/okf/rules/TF037.md +11 -0
  86. package/dist/okf/rules/TF038.md +11 -0
  87. package/dist/okf/rules/TF039.md +11 -0
  88. package/dist/okf/rules/TF040.md +11 -0
  89. package/dist/okf/rules/TF041.md +11 -0
  90. package/dist/okf/rules/TF042.md +11 -0
  91. package/dist/okf/rules/TF043.md +11 -0
  92. package/dist/okf/rules/TF044.md +11 -0
  93. package/dist/okf/rules/TF045.md +11 -0
  94. package/dist/op/activities/index.d.ts +4 -0
  95. package/dist/op/activities/index.d.ts.map +1 -1
  96. package/dist/op/activities/pin-rollout.d.ts +46 -0
  97. package/dist/op/activities/pin-rollout.d.ts.map +1 -0
  98. package/dist/op/activities/terraform.d.ts +24 -50
  99. package/dist/op/activities/terraform.d.ts.map +1 -1
  100. package/dist/op/activity-contracts.d.ts +66 -18
  101. package/dist/op/activity-contracts.d.ts.map +1 -1
  102. package/dist/op/builders.d.ts +9 -0
  103. package/dist/op/builders.d.ts.map +1 -1
  104. package/dist/pin/edit-ts.d.ts +21 -0
  105. package/dist/pin/edit-ts.d.ts.map +1 -0
  106. package/dist/pin/edit.d.ts +52 -0
  107. package/dist/pin/edit.d.ts.map +1 -0
  108. package/dist/pin/forge.d.ts +39 -0
  109. package/dist/pin/forge.d.ts.map +1 -0
  110. package/dist/pin/index.d.ts +22 -0
  111. package/dist/pin/index.d.ts.map +1 -0
  112. package/dist/pin/rollout.d.ts +116 -0
  113. package/dist/pin/rollout.d.ts.map +1 -0
  114. package/dist/pin/scan.d.ts +54 -0
  115. package/dist/pin/scan.d.ts.map +1 -0
  116. package/dist/pin/source.d.ts +93 -0
  117. package/dist/pin/source.d.ts.map +1 -0
  118. package/dist/pin/waves.d.ts +66 -0
  119. package/dist/pin/waves.d.ts.map +1 -0
  120. package/dist/plan-digest.d.ts +59 -0
  121. package/dist/plan-digest.d.ts.map +1 -0
  122. package/dist/plugin.d.ts.map +1 -1
  123. package/dist/rules/flat-attributes.ts +146 -0
  124. package/dist/rules/module-source.ts +56 -1
  125. package/dist/rules/terragrunt.ts +76 -0
  126. package/dist/rules/tf002.ts +1 -1
  127. package/dist/rules/tf030.ts +290 -0
  128. package/dist/rules/tf031.ts +370 -0
  129. package/dist/rules/tf032.ts +226 -0
  130. package/dist/rules/tf033.ts +160 -0
  131. package/dist/rules/tf034.ts +63 -0
  132. package/dist/rules/tf035.ts +74 -0
  133. package/dist/rules/tf036.ts +119 -0
  134. package/dist/rules/tf037.ts +101 -0
  135. package/dist/rules/tf038.ts +71 -0
  136. package/dist/rules/tf039.ts +64 -0
  137. package/dist/rules/tf040.ts +76 -0
  138. package/dist/rules/tf041.ts +56 -0
  139. package/dist/rules/tf042.ts +42 -0
  140. package/dist/rules/tf043.ts +49 -0
  141. package/dist/rules/tf044.ts +51 -0
  142. package/dist/rules/tf045.ts +47 -0
  143. package/dist/terragrunt/affected.d.ts +119 -0
  144. package/dist/terragrunt/affected.d.ts.map +1 -0
  145. package/dist/terragrunt/index.d.ts +11 -0
  146. package/dist/terragrunt/index.d.ts.map +1 -0
  147. package/dist/terragrunt/mocks.d.ts +147 -0
  148. package/dist/terragrunt/mocks.d.ts.map +1 -0
  149. package/dist/terragrunt/run.d.ts +130 -0
  150. package/dist/terragrunt/run.d.ts.map +1 -0
  151. package/dist/terragrunt/units.d.ts +103 -0
  152. package/dist/terragrunt/units.d.ts.map +1 -0
  153. package/dist/terragrunt/wave.d.ts +89 -0
  154. package/dist/terragrunt/wave.d.ts.map +1 -0
  155. package/package.json +13 -3
  156. package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
  157. package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
  158. package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
  159. package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
  160. package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
  161. package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
  162. package/src/__fixtures__/change-set/terraform.plan.json +589 -0
  163. package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
  164. package/src/__fixtures__/change-set/tofu.plan.json +588 -0
  165. package/src/__fixtures__/change-set/v1.tf.txt +21 -0
  166. package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
  167. package/src/__fixtures__/change-set/v2.tf.txt +21 -0
  168. package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
  169. package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
  170. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
  171. package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
  172. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
  173. package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
  174. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
  175. package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
  176. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
  177. package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
  178. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
  179. package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
  180. package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
  181. package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
  182. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
  183. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
  184. package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
  185. package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
  186. package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
  187. package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
  188. package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
  189. package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
  190. package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
  191. package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
  192. package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
  193. package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
  194. package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
  195. package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
  196. package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
  197. package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
  198. package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
  199. package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
  200. package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
  201. package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
  202. package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
  203. package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
  204. package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
  205. package/src/__fixtures__/terragrunt/record.ts +56 -0
  206. package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
  207. package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
  208. package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
  209. package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
  210. package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
  211. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
  212. package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
  213. package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
  214. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
  215. package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
  216. package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
  217. package/src/change-set-combined-run.test.ts +156 -0
  218. package/src/change-set.test.ts +151 -0
  219. package/src/change-set.ts +273 -0
  220. package/src/changed-roots.test.ts +58 -0
  221. package/src/changed-roots.ts +102 -0
  222. package/src/commands.ts +112 -0
  223. package/src/components/capability-plugin.test.ts +91 -0
  224. package/src/components/capability-plugin.ts +131 -0
  225. package/src/components/pr-loop.tofu.test.ts +244 -0
  226. package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
  227. package/src/composites/terraform-pin-rollout-op.ts +60 -0
  228. package/src/hcl/descend.ts +1 -1
  229. package/src/hcl/edges.ts +2 -2
  230. package/src/hcl/jsonencode.test.ts +226 -0
  231. package/src/hcl/jsonencode.ts +454 -0
  232. package/src/hcl/parse.test.ts +25 -0
  233. package/src/hcl/parse.ts +91 -6
  234. package/src/hcl/roots.ts +1 -1
  235. package/src/hcl/suppressions.ts +8 -2
  236. package/src/index.ts +18 -0
  237. package/src/lint/audit-catalog.ts +230 -0
  238. package/src/lint/audit-lineage.ts +155 -1
  239. package/src/lint/audit.test.ts +3 -1
  240. package/src/lint/post-synth/estate-2284.test.ts +68 -0
  241. package/src/lint/post-synth/fixtures/TF030/negative-rules.tf +45 -0
  242. package/src/lint/post-synth/fixtures/TF030/negative.tf +55 -0
  243. package/src/lint/post-synth/fixtures/TF030/not-determined-port.tf +28 -0
  244. package/src/lint/post-synth/fixtures/TF030/not-determined-standalone.tf +32 -0
  245. package/src/lint/post-synth/fixtures/TF030/not-determined-vpc-rule.tf +21 -0
  246. package/src/lint/post-synth/fixtures/TF030/not-determined.tf +39 -0
  247. package/src/lint/post-synth/fixtures/TF030/positive-ipv6.tf +44 -0
  248. package/src/lint/post-synth/fixtures/TF030/positive-mixed.tf +36 -0
  249. package/src/lint/post-synth/fixtures/TF030/positive-standalone.tf +25 -0
  250. package/src/lint/post-synth/fixtures/TF030/positive.tf +43 -0
  251. package/src/lint/post-synth/fixtures/TF031/negative.tf +89 -0
  252. package/src/lint/post-synth/fixtures/TF031/not-determined.tf +139 -0
  253. package/src/lint/post-synth/fixtures/TF031/positive-data-source.tf +57 -0
  254. package/src/lint/post-synth/fixtures/TF031/positive-heredoc.tf +149 -0
  255. package/src/lint/post-synth/fixtures/TF031/positive-jsonencode.tf +115 -0
  256. package/src/lint/post-synth/fixtures/TF032/negative-heredoc.tf +20 -0
  257. package/src/lint/post-synth/fixtures/TF032/negative-references.tf +214 -0
  258. package/src/lint/post-synth/fixtures/TF032/negative.tf +99 -0
  259. package/src/lint/post-synth/fixtures/TF032/positive-heredoc.tf +22 -0
  260. package/src/lint/post-synth/fixtures/TF032/positive-sidecar.tf +197 -0
  261. package/src/lint/post-synth/fixtures/TF032/positive.tf +54 -0
  262. package/src/lint/post-synth/fixtures/TF033/negative.tf +39 -0
  263. package/src/lint/post-synth/fixtures/TF033/not-determined-aurora.tf +25 -0
  264. package/src/lint/post-synth/fixtures/TF033/positive-absent.tf +93 -0
  265. package/src/lint/post-synth/fixtures/TF033/positive.tf +50 -0
  266. package/src/lint/post-synth/fixtures/TF034/negative.tf +24 -0
  267. package/src/lint/post-synth/fixtures/TF034/positive-absent.tf +36 -0
  268. package/src/lint/post-synth/fixtures/TF034/positive.tf +14 -0
  269. package/src/lint/post-synth/fixtures/TF035/negative-absent.tf +43 -0
  270. package/src/lint/post-synth/fixtures/TF035/negative.tf +54 -0
  271. package/src/lint/post-synth/fixtures/TF035/positive.tf +37 -0
  272. package/src/lint/post-synth/fixtures/TF036/negative.tf +61 -0
  273. package/src/lint/post-synth/fixtures/TF036/not-determined.tf +28 -0
  274. package/src/lint/post-synth/fixtures/TF036/positive.tf +38 -0
  275. package/src/lint/post-synth/fixtures/TF037/negative-exclusion.tf +63 -0
  276. package/src/lint/post-synth/fixtures/TF037/negative.tf +27 -0
  277. package/src/lint/post-synth/fixtures/TF037/not-determined.tf +27 -0
  278. package/src/lint/post-synth/fixtures/TF037/positive-absent.tf +15 -0
  279. package/src/lint/post-synth/fixtures/TF037/positive.tf +26 -0
  280. package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
  282. package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
  283. package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
  284. package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
  285. package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
  286. package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
  287. package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
  288. package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
  289. package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
  290. package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
  291. package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
  292. package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
  293. package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
  294. package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
  295. package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
  296. package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
  297. package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
  298. package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
  299. package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
  300. package/src/lint/post-synth/fixtures/estate-2284/clean/main.tf +132 -0
  301. package/src/lint/post-synth/fixtures/estate-2284/violating/main.tf +112 -0
  302. package/src/lint/post-synth/flat-attributes.ts +146 -0
  303. package/src/lint/post-synth/index.ts +32 -0
  304. package/src/lint/post-synth/module-source.test.ts +42 -1
  305. package/src/lint/post-synth/module-source.ts +56 -1
  306. package/src/lint/post-synth/terragrunt.ts +76 -0
  307. package/src/lint/post-synth/tf002.ts +1 -1
  308. package/src/lint/post-synth/tf030.test.ts +219 -0
  309. package/src/lint/post-synth/tf030.ts +290 -0
  310. package/src/lint/post-synth/tf031.test.ts +229 -0
  311. package/src/lint/post-synth/tf031.ts +370 -0
  312. package/src/lint/post-synth/tf032.test.ts +127 -0
  313. package/src/lint/post-synth/tf032.ts +226 -0
  314. package/src/lint/post-synth/tf033.test.ts +126 -0
  315. package/src/lint/post-synth/tf033.ts +160 -0
  316. package/src/lint/post-synth/tf034.test.ts +71 -0
  317. package/src/lint/post-synth/tf034.ts +63 -0
  318. package/src/lint/post-synth/tf035.test.ts +68 -0
  319. package/src/lint/post-synth/tf035.ts +74 -0
  320. package/src/lint/post-synth/tf036.test.ts +87 -0
  321. package/src/lint/post-synth/tf036.ts +119 -0
  322. package/src/lint/post-synth/tf037.test.ts +88 -0
  323. package/src/lint/post-synth/tf037.ts +101 -0
  324. package/src/lint/post-synth/tf038.test.ts +51 -0
  325. package/src/lint/post-synth/tf038.ts +71 -0
  326. package/src/lint/post-synth/tf039.test.ts +40 -0
  327. package/src/lint/post-synth/tf039.ts +64 -0
  328. package/src/lint/post-synth/tf040.test.ts +55 -0
  329. package/src/lint/post-synth/tf040.ts +76 -0
  330. package/src/lint/post-synth/tf041.test.ts +50 -0
  331. package/src/lint/post-synth/tf041.ts +56 -0
  332. package/src/lint/post-synth/tf042.test.ts +19 -0
  333. package/src/lint/post-synth/tf042.ts +42 -0
  334. package/src/lint/post-synth/tf043.test.ts +65 -0
  335. package/src/lint/post-synth/tf043.ts +49 -0
  336. package/src/lint/post-synth/tf044.test.ts +27 -0
  337. package/src/lint/post-synth/tf044.ts +51 -0
  338. package/src/lint/post-synth/tf045.test.ts +30 -0
  339. package/src/lint/post-synth/tf045.ts +47 -0
  340. package/src/op/activities/index.ts +4 -0
  341. package/src/op/activities/pin-rollout.ts +77 -0
  342. package/src/op/activities/terraform.test.ts +13 -0
  343. package/src/op/activities/terraform.ts +46 -86
  344. package/src/op/activity-contracts.test.ts +4 -1
  345. package/src/op/activity-contracts.ts +42 -0
  346. package/src/op/builders.ts +13 -0
  347. package/src/pin/bundle.test.ts +53 -0
  348. package/src/pin/edit-ts.test.ts +39 -0
  349. package/src/pin/edit-ts.ts +77 -0
  350. package/src/pin/edit.test.ts +199 -0
  351. package/src/pin/edit.ts +157 -0
  352. package/src/pin/forge.ts +85 -0
  353. package/src/pin/index.ts +22 -0
  354. package/src/pin/rollout.test.ts +232 -0
  355. package/src/pin/rollout.ts +451 -0
  356. package/src/pin/scan.ts +309 -0
  357. package/src/pin/source.ts +206 -0
  358. package/src/pin/waves.test.ts +101 -0
  359. package/src/pin/waves.ts +125 -0
  360. package/src/plan-digest.ts +99 -0
  361. package/src/plan-summary.test.ts +154 -0
  362. package/src/plugin.ts +25 -2
  363. package/src/terragrunt/affected.acceptance.test.ts +83 -0
  364. package/src/terragrunt/affected.test.ts +177 -0
  365. package/src/terragrunt/affected.ts +348 -0
  366. package/src/terragrunt/index.ts +11 -0
  367. package/src/terragrunt/mocks.acceptance.test.ts +105 -0
  368. package/src/terragrunt/mocks.test.ts +309 -0
  369. package/src/terragrunt/mocks.ts +324 -0
  370. package/src/terragrunt/run.ts +369 -0
  371. package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
  372. package/src/terragrunt/terragrunt.test.ts +332 -0
  373. package/src/terragrunt/units.ts +235 -0
  374. package/src/terragrunt/wave.ts +181 -0
@@ -0,0 +1,454 @@
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
+
42
+ export type JsonencodeRead =
43
+ /** The argument is a fully literal HCL object/tuple/string/number/bool/null tree. */
44
+ | { kind: "literal"; value: unknown }
45
+ /** `jsonencode(...)` whose argument contains a reference, function call, for-expression, splat, conditional, etc. */
46
+ | { kind: "not-determined"; reason: string }
47
+ /** The value is not a single whole-string `${jsonencode(...)}` at all. */
48
+ | { kind: "not-jsonencode" };
49
+
50
+ export interface ReadJsonencodeOptions {
51
+ /**
52
+ * Read past a non-literal VALUE instead of giving up on the whole argument.
53
+ * Each such value (a reference, a function call, a conditional, an
54
+ * interpolated string, a heredoc, any other expression) becomes an
55
+ * {@link UnknownLeaf} in the returned tree, so a rule can still read the
56
+ * literal fields beside it: `Action = "*"` next to
57
+ * `Resource = aws_s3_bucket.x.arn`, or a container's literal `environment`
58
+ * next to `image = var.image`.
59
+ *
60
+ * The skip is narrow. It consumes one value expression, balancing `()[]{}`
61
+ * and quoted strings, up to the next `,`, newline or closing bracket at its
62
+ * own depth. A member whose KEY is an expression (an interpolated quoted
63
+ * key or a parenthesised computed key) is dropped from its object and listed
64
+ * under {@link UNKNOWN_KEYS} on that object. Damage to the STRUCTURE is
65
+ * still `not-determined`: an expression as the whole argument, including
66
+ * a for-expression or a splat (`jsonencode(local.policy)`,
67
+ * `jsonencode(var.x[*])`), since then no key or element is known.
68
+ */
69
+ unknownLeaves?: boolean;
70
+ }
71
+
72
+ /** Marks a value position `readJsonencode(raw, { unknownLeaves: true })` could not read. */
73
+ export const UNKNOWN: unique symbol = Symbol("jsonencode.unknown");
74
+
75
+ /** A value position holding an expression; `reason` names what was found there. */
76
+ export interface UnknownLeaf {
77
+ readonly [UNKNOWN]: true;
78
+ readonly reason: string;
79
+ /**
80
+ * The skipped expression's own source text, trimmed (`aws_s3_bucket.x.arn`,
81
+ * `"arn:aws:s3:::${var.bucket}/*"` with its quotes). A rule may read it to
82
+ * rule a value OUT (a quoted template whose literal text already differs
83
+ * from the value it looks for), never to evaluate it.
84
+ */
85
+ readonly source: string;
86
+ }
87
+
88
+ /**
89
+ * Marks an object, read with `unknownLeaves`, that had members whose KEY is an
90
+ * expression (`"router-${var.n}.rule" = ...`, `(var.k) = ...`). Those members
91
+ * are dropped from the object and their reasons listed here, so a reader can
92
+ * tell "this key is absent" from "some key here could not be read".
93
+ */
94
+ export const UNKNOWN_KEYS: unique symbol = Symbol("jsonencode.unknownKeys");
95
+
96
+ /** Whether `obj` is an object that dropped one or more members with an expression key. */
97
+ export function hasUnknownKeys(obj: unknown): obj is Record<string, unknown> & { [UNKNOWN_KEYS]: string[] } {
98
+ return (
99
+ typeof obj === "object" &&
100
+ obj !== null &&
101
+ Array.isArray((obj as Record<symbol, unknown>)[UNKNOWN_KEYS]) &&
102
+ ((obj as Record<symbol, unknown>)[UNKNOWN_KEYS] as unknown[]).length > 0
103
+ );
104
+ }
105
+
106
+ /** Whether `v` is a value position the reader skipped. */
107
+ export function isUnknown(v: unknown): v is UnknownLeaf {
108
+ return typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[UNKNOWN] === true;
109
+ }
110
+
111
+ const PREFIX = /^\$\{\s*jsonencode\s*\(/;
112
+ const SUFFIX = /\)\s*\}$/;
113
+
114
+ /** Read `raw` (an hcl2json attribute value) as a `jsonencode(...)` call. */
115
+ export function readJsonencode(raw: unknown, options: ReadJsonencodeOptions = {}): JsonencodeRead {
116
+ if (typeof raw !== "string") return { kind: "not-jsonencode" };
117
+ const prefix = PREFIX.exec(raw);
118
+ if (!prefix || !SUFFIX.test(raw)) return { kind: "not-jsonencode" };
119
+
120
+ const reader = new Reader(raw, prefix[0].length, options.unknownLeaves === true);
121
+ try {
122
+ const value = reader.value(0);
123
+ reader.skipSpace();
124
+ // The argument must end at the call's own `)`, then the interpolation's `}`.
125
+ reader.expect(")", "a second argument or an expression after the literal");
126
+ reader.skipSpace();
127
+ reader.expect("}", "text after the jsonencode call");
128
+ if (!reader.atEnd()) throw new NotLiteral("text after the jsonencode call");
129
+ return { kind: "literal", value };
130
+ } catch (err) {
131
+ if (err instanceof NotLiteral) return { kind: "not-determined", reason: err.message };
132
+ throw err;
133
+ }
134
+ }
135
+
136
+ class NotLiteral extends Error {}
137
+
138
+ /** Damage to the shape itself: never recovered by skipping a leaf. */
139
+ class Structural extends NotLiteral {}
140
+
141
+ /** An object key that is an expression; recoverable by dropping the member under `unknownLeaves`. */
142
+ class KeyNotLiteral extends Structural {}
143
+
144
+ const IDENT = /[A-Za-z_][A-Za-z0-9_-]*/y;
145
+ const NUMBER = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
146
+
147
+ /** A cursor over the source text of the jsonencode argument. */
148
+ class Reader {
149
+ constructor(
150
+ private readonly src: string,
151
+ private pos: number,
152
+ private readonly unknownLeaves = false,
153
+ ) {}
154
+
155
+ atEnd(): boolean {
156
+ return this.pos >= this.src.length;
157
+ }
158
+
159
+ private peek(): string {
160
+ return this.src[this.pos] ?? "";
161
+ }
162
+
163
+ expect(ch: string, what: string): void {
164
+ if (this.peek() !== ch) throw new NotLiteral(what === "" ? `expected "${ch}"` : what);
165
+ this.pos++;
166
+ }
167
+
168
+ /** Whitespace, newlines and the three HCL comment forms. */
169
+ skipSpace(newlines = true): void {
170
+ for (;;) {
171
+ const c = this.peek();
172
+ if (c === " " || c === "\t" || c === "\r" || (newlines && c === "\n")) {
173
+ this.pos++;
174
+ } else if (c === "#" || (c === "/" && this.src[this.pos + 1] === "/")) {
175
+ const end = this.src.indexOf("\n", this.pos);
176
+ this.pos = end === -1 ? this.src.length : end;
177
+ } else if (c === "/" && this.src[this.pos + 1] === "*") {
178
+ const end = this.src.indexOf("*/", this.pos + 2);
179
+ if (end === -1) throw new NotLiteral("an unterminated comment");
180
+ this.pos = end + 2;
181
+ } else {
182
+ return;
183
+ }
184
+ }
185
+ }
186
+
187
+ /** One value. `depth` is 0 for the whole argument, which is never skipped. */
188
+ value(depth: number): unknown {
189
+ this.skipSpace();
190
+ if (!this.unknownLeaves || depth === 0) return this.literal(depth);
191
+ const start = this.pos;
192
+ try {
193
+ return this.literal(depth);
194
+ } catch (err) {
195
+ if (!(err instanceof NotLiteral) || err instanceof Structural) throw err;
196
+ this.pos = start;
197
+ this.skipExpression();
198
+ return { [UNKNOWN]: true, reason: err.message, source: this.src.slice(start, this.pos).trim() } satisfies UnknownLeaf;
199
+ }
200
+ }
201
+
202
+ private literal(depth: number): unknown {
203
+ const c = this.peek();
204
+ let v: unknown;
205
+ if (c === "{") v = this.object(depth + 1);
206
+ else if (c === "[") v = this.tuple(depth + 1);
207
+ else if (c === '"') v = this.string();
208
+ else if (c === "<" && this.src.startsWith("<<", this.pos)) throw new NotLiteral("a heredoc");
209
+ else if (c === "(") throw new NotLiteral("a parenthesised expression");
210
+ else if (c === "-" || (c >= "0" && c <= "9")) v = this.number();
211
+ else v = this.keyword();
212
+ this.afterValue();
213
+ return v;
214
+ }
215
+
216
+ /**
217
+ * A literal is complete only if what follows it closes or separates. An
218
+ * operator, a conditional, an attribute access or an index after it makes
219
+ * the whole thing an expression.
220
+ */
221
+ private afterValue(): void {
222
+ this.skipSpace(false);
223
+ const c = this.peek();
224
+ if (c === "" || c === "\n" || c === "," || c === "}" || c === "]" || c === ")") return;
225
+ const next = this.src[this.pos + 1];
226
+ if (c === "#" || (c === "/" && (next === "/" || next === "*"))) return; // a comment, consumed by the caller's skipSpace
227
+ if (c === "?") throw new NotLiteral("a conditional expression");
228
+ if (c === "." || c === "[") throw new NotLiteral("an attribute access, index or splat");
229
+ throw new NotLiteral(`an operator or expression ("${c}")`);
230
+ }
231
+
232
+ private object(depth: number): Record<string, unknown> {
233
+ this.expect("{", "");
234
+ const out: Record<string, unknown> = {};
235
+ for (;;) {
236
+ this.skipSpace();
237
+ if (this.peek() === "}") {
238
+ this.pos++;
239
+ return out;
240
+ }
241
+ const quoted = this.peek() === '"';
242
+ const memberStart = this.pos;
243
+ let key: string;
244
+ try {
245
+ key = this.key();
246
+ } catch (err) {
247
+ if (!(err instanceof KeyNotLiteral) || !this.unknownLeaves) throw err;
248
+ // Drop the whole member: the skip runs from the key through its value
249
+ // to the separator, balancing brackets and quotes on the way.
250
+ this.pos = memberStart;
251
+ this.skipExpression();
252
+ const bag = out as Record<symbol, string[]>;
253
+ (bag[UNKNOWN_KEYS] ??= []).push(err.message);
254
+ this.skipSpace(false);
255
+ if (this.peek() === ",") this.pos++;
256
+ continue;
257
+ }
258
+ this.skipSpace(false);
259
+ const sep = this.peek();
260
+ if (sep !== "=" && sep !== ":") {
261
+ // A for-expression is a VALUE: under unknownLeaves the enclosing
262
+ // value() skips it as a leaf; as the whole argument it is not determined.
263
+ if (!quoted && key === "for") throw new NotLiteral("a for-expression");
264
+ throw new Structural(`an expression where "${key}" needs = or :`);
265
+ }
266
+ this.pos++;
267
+ out[key] = this.value(depth);
268
+ this.skipSpace(false);
269
+ if (this.peek() === ",") this.pos++;
270
+ }
271
+ }
272
+
273
+ private key(): string {
274
+ const c = this.peek();
275
+ if (c === '"') {
276
+ try {
277
+ return this.string();
278
+ } catch (err) {
279
+ if (err instanceof NotLiteral) throw new KeyNotLiteral(`an object key holding ${err.message.replace(/^a string with /, "")}`);
280
+ throw err;
281
+ }
282
+ }
283
+ if (c === "(") throw new KeyNotLiteral("a computed object key");
284
+ IDENT.lastIndex = this.pos;
285
+ const m = IDENT.exec(this.src);
286
+ if (!m) throw new Structural(`an unexpected "${c}" where an object key belongs`);
287
+ this.pos += m[0].length;
288
+ return m[0];
289
+ }
290
+
291
+ private tuple(depth: number): unknown[] {
292
+ this.expect("[", "");
293
+ this.skipSpace();
294
+ if (/^for\s/.test(this.src.slice(this.pos, this.pos + 4))) throw new NotLiteral("a for-expression");
295
+ const out: unknown[] = [];
296
+ for (;;) {
297
+ this.skipSpace();
298
+ if (this.peek() === "]") {
299
+ this.pos++;
300
+ return out;
301
+ }
302
+ out.push(this.value(depth));
303
+ this.skipSpace();
304
+ if (this.peek() === ",") this.pos++;
305
+ else if (this.peek() !== "]") throw new NotLiteral("a tuple item followed by an expression");
306
+ }
307
+ }
308
+
309
+ /** A quoted HCL string. A `${` or `%{` (not escaped as `$${`/`%%{`) is a template. */
310
+ private string(): string {
311
+ this.expect('"', "");
312
+ let out = "";
313
+ for (;;) {
314
+ const c = this.peek();
315
+ if (c === "") throw new NotLiteral("an unterminated string");
316
+ if (c === "\n") throw new NotLiteral("a newline inside a quoted string");
317
+ if (c === '"') {
318
+ this.pos++;
319
+ return out;
320
+ }
321
+ if ((c === "$" || c === "%") && this.src[this.pos + 1] === c && this.src[this.pos + 2] === "{") {
322
+ out += `${c}{`;
323
+ this.pos += 3;
324
+ continue;
325
+ }
326
+ if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
327
+ throw new NotLiteral(c === "$" ? "a string with a ${...} interpolation" : "a string with a %{...} directive");
328
+ }
329
+ if (c === "\\") {
330
+ out += this.escape();
331
+ continue;
332
+ }
333
+ out += c;
334
+ this.pos++;
335
+ }
336
+ }
337
+
338
+ private escape(): string {
339
+ const e = this.src[this.pos + 1] ?? "";
340
+ this.pos += 2;
341
+ switch (e) {
342
+ case "n":
343
+ return "\n";
344
+ case "r":
345
+ return "\r";
346
+ case "t":
347
+ return "\t";
348
+ case '"':
349
+ return '"';
350
+ case "\\":
351
+ return "\\";
352
+ case "u":
353
+ case "U": {
354
+ const len = e === "u" ? 4 : 8;
355
+ const hex = this.src.slice(this.pos, this.pos + len);
356
+ if (!/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== len) throw new NotLiteral("a malformed unicode escape");
357
+ this.pos += len;
358
+ return String.fromCodePoint(parseInt(hex, 16));
359
+ }
360
+ default:
361
+ throw new NotLiteral(`an unknown string escape "\\${e}"`);
362
+ }
363
+ }
364
+
365
+ private number(): number {
366
+ NUMBER.lastIndex = this.pos;
367
+ const m = NUMBER.exec(this.src);
368
+ if (!m) throw new NotLiteral("a unary operator on an expression");
369
+ this.pos += m[0].length;
370
+ return Number(m[0]);
371
+ }
372
+
373
+ private keyword(): unknown {
374
+ IDENT.lastIndex = this.pos;
375
+ const m = IDENT.exec(this.src);
376
+ if (!m) throw new NotLiteral(`an unexpected "${this.peek()}"`);
377
+ const word = m[0];
378
+ this.pos += word.length;
379
+ if (word === "true") return true;
380
+ if (word === "false") return false;
381
+ if (word === "null") return null;
382
+ this.skipSpace(false);
383
+ if (this.peek() === "(") throw new NotLiteral(`a function call (${word}(...))`);
384
+ if (word === "for") throw new NotLiteral("a for-expression");
385
+ return this.reference(word);
386
+ }
387
+
388
+ /**
389
+ * Skip one value expression from the cursor: balance `()[]{}` and quoted
390
+ * strings (with their `${...}` interpolations), stop at a `,`, a newline, a
391
+ * comment or a closing bracket at depth 0. A heredoc is skipped to its
392
+ * closing marker line.
393
+ */
394
+ private skipExpression(): void {
395
+ let depth = 0;
396
+ while (!this.atEnd()) {
397
+ const c = this.peek();
398
+ if (depth === 0) {
399
+ if (c === "," || c === "\n" || c === "}" || c === "]" || c === ")" || c === "#") return;
400
+ if (c === "/" && (this.src[this.pos + 1] === "/" || this.src[this.pos + 1] === "*")) return;
401
+ }
402
+ if (c === '"') this.skipQuoted();
403
+ else if (c === "<" && this.src.startsWith("<<", this.pos)) this.skipHeredoc();
404
+ else {
405
+ if (c === "(" || c === "[" || c === "{") depth++;
406
+ else if (c === ")" || c === "]" || c === "}") depth--;
407
+ this.pos++;
408
+ }
409
+ }
410
+ }
411
+
412
+ /** Skip a quoted string, including any `${...}` interpolations (which may hold strings). */
413
+ private skipQuoted(): void {
414
+ this.pos++;
415
+ while (!this.atEnd()) {
416
+ const c = this.peek();
417
+ if (c === "\\") this.pos += 2;
418
+ else if (c === '"') {
419
+ this.pos++;
420
+ return;
421
+ } else if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
422
+ this.pos += 2;
423
+ let depth = 1;
424
+ while (!this.atEnd() && depth > 0) {
425
+ const d = this.peek();
426
+ if (d === '"') {
427
+ this.skipQuoted();
428
+ continue;
429
+ }
430
+ if (d === "{") depth++;
431
+ else if (d === "}") depth--;
432
+ this.pos++;
433
+ }
434
+ } else this.pos++;
435
+ }
436
+ throw new Structural("an unterminated string");
437
+ }
438
+
439
+ private skipHeredoc(): void {
440
+ const head = /<<-?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\n/y;
441
+ head.lastIndex = this.pos;
442
+ const m = head.exec(this.src);
443
+ if (!m) throw new Structural("a malformed heredoc");
444
+ const close = new RegExp(`^[ \\t]*${m[1]}[ \\t]*$`, "m");
445
+ const rest = this.src.slice(this.pos + m[0].length);
446
+ const end = close.exec(rest);
447
+ if (!end) throw new Structural("an unterminated heredoc");
448
+ this.pos += m[0].length + end.index + end[0].length;
449
+ }
450
+
451
+ private reference(word: string): never {
452
+ throw new NotLiteral(`a reference (${word}${this.peek() === "." ? "..." : ""})`);
453
+ }
454
+ }
@@ -67,3 +67,28 @@ describe("blocksToEntities: suppression attachment (chant #2111)", () => {
67
67
  expect(second.suppressions).toBeUndefined();
68
68
  });
69
69
  });
70
+
71
+ describe("Terragrunt config files (#3417)", () => {
72
+ const files = [
73
+ {
74
+ name: "terragrunt.hcl",
75
+ source: 'terraform {\n source = "../m"\n}\ndependency "vpc" {\n config_path = "../vpc"\n}\nremote_state {\n backend = "s3"\n}\n',
76
+ },
77
+ ];
78
+
79
+ test("terragrunt.hcl and root.hcl become Terragrunt entities, never module entities", async () => {
80
+ const entities = await blocksToEntities(files, "unit");
81
+ expect([...entities.values()].map((e) => e.entityType).sort()).toEqual([
82
+ "Terraform::TerragruntConfig",
83
+ "Terraform::TerragruntDependency",
84
+ "Terraform::TerragruntRemoteState",
85
+ "Terraform::TerragruntSource",
86
+ ]);
87
+ expect([...entities.keys()]).toContain("unit/dependency.vpc");
88
+ });
89
+
90
+ test("the same text under a .tf name is read as a Terraform terraform block", async () => {
91
+ const entities = await blocksToEntities([{ name: "main.tf", source: 'terraform {\n required_version = ">= 1.5"\n}\n' }], "unit");
92
+ expect([...entities.values()].map((e) => e.entityType)).toEqual(["Terraform::Terraform"]);
93
+ });
94
+ });
package/src/hcl/parse.ts CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { existsSync, readdirSync, readFileSync } from "node:fs";
22
22
  import { join } from "node:path";
23
- import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/parse";
23
+ import { loadHcl2json, type Hcl2Json } from "@intentius/chant/terraform/hcl2json";
24
24
  import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
25
25
  import type { EntityReference } from "@intentius/chant/graph-ir";
26
26
  import type { SuppressionDirective } from "@intentius/chant/lint/suppressions";
@@ -83,6 +83,13 @@ export interface TerraformEntity extends Declarable {
83
83
  readonly mode?: TerraformRootMode;
84
84
  /** The declared estate name. Present only when `mode` is `"live"`. */
85
85
  readonly estate?: string;
86
+ /**
87
+ * Whether `.terraform.lock.hcl` sits beside the root's `.tf` files (#3190,
88
+ * TF040). Stamped on every entity of a root parsed from a directory, and
89
+ * absent when the parse had no directory to look in (a fetched
90
+ * repository, inline source), which reads as "not determined".
91
+ */
92
+ readonly lockFile?: boolean;
86
93
  /** `terraform.roots.<name>.workspace`, recorded so TF025 can flag a non-default one on a live root. */
87
94
  readonly workspace?: string;
88
95
  /**
@@ -166,6 +173,26 @@ export const LOCALS_TYPE = "Terraform::Locals";
166
173
  /** A `live { estate = "..." }` block, or an `estate.chdf.hcl` sidecar's content (#2103). */
167
174
  export const LIVE_TYPE = "Terraform::Live";
168
175
 
176
+ /**
177
+ * Terragrunt config entities (#3417). A `terragrunt.hcl` or `root.hcl` is not
178
+ * a Terraform module, so its blocks are read into these types and never into
179
+ * `TERRAFORM_TYPE`: TF001's `required_version` check and the rest of the
180
+ * module rules keep to the `.tf` files.
181
+ */
182
+ export const TERRAGRUNT_CONFIG_TYPE = "Terraform::TerragruntConfig";
183
+ export const TERRAGRUNT_SOURCE_TYPE = "Terraform::TerragruntSource";
184
+ export const TERRAGRUNT_DEPENDENCY_TYPE = "Terraform::TerragruntDependency";
185
+ export const TERRAGRUNT_REMOTE_STATE_TYPE = "Terraform::TerragruntRemoteState";
186
+ export const TERRAGRUNT_GENERATE_TYPE = "Terraform::TerragruntGenerate";
187
+
188
+ /** The Terragrunt config files read, by their default names. A `--config` rename is not followed. */
189
+ export const TERRAGRUNT_FILENAMES: readonly string[] = ["root.hcl", "terragrunt.hcl"];
190
+
191
+ /** Whether `name` (a bare filename) is a Terragrunt config file this lexicon reads. */
192
+ export function isTerragruntFileName(name: string): boolean {
193
+ return TERRAGRUNT_FILENAMES.includes(name);
194
+ }
195
+
169
196
  /** The sidecar filename choudoufu reads an estate declaration from when no in-block `live { }` is used. */
170
197
  export const LIVE_SIDECAR_FILENAME = "estate.chdf.hcl";
171
198
 
@@ -260,6 +287,11 @@ export function listTerraformFiles(dir: string): string[] {
260
287
  .sort();
261
288
  }
262
289
 
290
+ /** The Terragrunt config files directly under `dir` (`root.hcl`, `terragrunt.hcl`), in filename order like the audit's bundle. */
291
+ export function listTerragruntFiles(dir: string): string[] {
292
+ return TERRAGRUNT_FILENAMES.filter((name) => existsSync(join(dir, name)));
293
+ }
294
+
263
295
  /**
264
296
  * Read the `estate.chdf.hcl` sidecar beside `dir`'s `.tf` files, if present.
265
297
  * Sibling to {@link listTerraformFiles}: the sidecar is plain HCL (a bare
@@ -313,8 +345,13 @@ export interface TerraformRootModeOptions {
313
345
  * declaration silently overrides.
314
346
  */
315
347
  configEstate?: string;
348
+ /** Stamp `props.lockFile` on every entity (#3190). Set by the two directory-backed parses only. */
349
+ lockFile?: boolean;
316
350
  }
317
351
 
352
+ /** The lock file `terraform init` writes beside a root's `.tf` files. */
353
+ export const LOCK_FILENAME = ".terraform.lock.hcl";
354
+
318
355
  /**
319
356
  * Parse a set of files into entities keyed `<root>/<address>`. Two blocks that
320
357
  * genuinely share an address (two `locals` blocks, or the same address in two
@@ -351,8 +388,15 @@ export async function blocksToEntities(
351
388
  const entities = new Map<string, Declarable>();
352
389
  const prefix = callers.length > 0 ? `${root}/${callers.join("/")}` : root;
353
390
 
354
- const add = (entityType: string, address: string, body: unknown, file: TerraformFile, scan: FileScan): void => {
355
- const { line, suppressions } = directivesFor(scan, address);
391
+ const add = (
392
+ entityType: string,
393
+ address: string,
394
+ body: unknown,
395
+ file: TerraformFile,
396
+ scan: FileScan,
397
+ scanAddress: string = address,
398
+ ): void => {
399
+ const { line, suppressions } = directivesFor(scan, scanAddress);
356
400
  const entity = terraformEntity(
357
401
  entityType,
358
402
  address,
@@ -418,6 +462,10 @@ export async function blocksToEntities(
418
462
  for (const file of files) {
419
463
  const tree = (await parser.parse(file.name, file.source)) as Record<string, unknown>;
420
464
  const scan = scanSuppressions(file.name, file.source);
465
+ if (isTerragruntFileName(file.name)) {
466
+ terragruntBlocks(tree, file, scan, add);
467
+ continue;
468
+ }
421
469
  unlabelled(tree, "terraform", TERRAFORM_TYPE, file, scan);
422
470
  for (const liveBody of liveBlocksIn(tree)) add(LIVE_TYPE, "live", liveBody, file, scan);
423
471
  unlabelled(tree, "locals", LOCALS_TYPE, file, scan);
@@ -450,7 +498,12 @@ export async function blocksToEntities(
450
498
  const te = entity as TerraformEntity;
451
499
  const stamped: TerraformEntity = {
452
500
  ...te,
453
- props: { ...te.props, mode, ...(mode === "live" ? { estate } : {}) },
501
+ props: {
502
+ ...te.props,
503
+ mode,
504
+ ...(mode === "live" ? { estate } : {}),
505
+ ...(modeOptions?.lockFile !== undefined ? { lockFile: modeOptions.lockFile } : {}),
506
+ },
454
507
  };
455
508
  entities.set(key, stamped);
456
509
  }
@@ -458,6 +511,34 @@ export async function blocksToEntities(
458
511
  return entities;
459
512
  }
460
513
 
514
+ /**
515
+ * One Terragrunt config file's blocks, as entities (#3417): the file's own
516
+ * settings (`terragrunt_version_constraint` and the `include` labels), the
517
+ * `terraform` block that names the unit's source, `remote_state`, each
518
+ * `dependency` and each `generate`. Everything else in the file (`inputs`,
519
+ * `locals`, `iam_role`) is left unread.
520
+ */
521
+ function terragruntBlocks(
522
+ tree: Record<string, unknown>,
523
+ file: TerraformFile,
524
+ scan: FileScan,
525
+ add: (entityType: string, address: string, body: unknown, file: TerraformFile, scan: FileScan, scanAddress?: string) => void,
526
+ ): void {
527
+ const config: BlockBody = { includes: Object.keys(asRecord(tree["include"])) };
528
+ if (typeof tree["terragrunt_version_constraint"] === "string") {
529
+ config["terragrunt_version_constraint"] = tree["terragrunt_version_constraint"];
530
+ }
531
+ add(TERRAGRUNT_CONFIG_TYPE, "terragrunt", config, file, scan, "");
532
+ for (const body of asArray(tree["terraform"])) add(TERRAGRUNT_SOURCE_TYPE, "terragrunt.terraform", body, file, scan, "terraform");
533
+ for (const body of asArray(tree["remote_state"])) add(TERRAGRUNT_REMOTE_STATE_TYPE, "remote_state", body, file, scan);
534
+ for (const [name, bodies] of Object.entries(asRecord(tree["dependency"]))) {
535
+ for (const body of asArray(bodies)) add(TERRAGRUNT_DEPENDENCY_TYPE, `dependency.${name}`, body, file, scan);
536
+ }
537
+ for (const [name, bodies] of Object.entries(asRecord(tree["generate"]))) {
538
+ for (const body of asArray(bodies)) add(TERRAGRUNT_GENERATE_TYPE, `generate.${name}`, body, file, scan);
539
+ }
540
+ }
541
+
461
542
  /**
462
543
  * Parse a root module directory. Reads every `.tf` directly under `dir`, plus
463
544
  * the `estate.chdf.hcl` sidecar beside them when present. Throws whatever the
@@ -471,7 +552,9 @@ export async function parseTerraformRootDir(
471
552
  modeOptions?: TerraformRootModeOptions,
472
553
  callers: readonly string[] = [],
473
554
  ): Promise<Map<string, Declarable>> {
474
- const files = listTerraformFiles(dir).map((name) => ({
555
+ // A child module is a Terraform module, never a Terragrunt unit (#3417).
556
+ const names = [...listTerraformFiles(dir), ...(callers.length === 0 ? listTerragruntFiles(dir) : [])];
557
+ const files = names.map((name) => ({
475
558
  name,
476
559
  source: readFileSync(join(dir, name), "utf-8"),
477
560
  }));
@@ -479,7 +562,9 @@ export async function parseTerraformRootDir(
479
562
  // estate a ROOT runs against, and choudoufu reads it from the root's own
480
563
  // directory only. See `./descend.ts`.
481
564
  const sidecar = callers.length === 0 ? readLiveSidecarFile(dir) : undefined;
482
- return blocksToEntities(files, root, hcl2json, modeOptions, sidecar, callers);
565
+ // Only the root's own directory carries a lock file that matters (#3190).
566
+ const opts = callers.length === 0 ? { ...modeOptions, lockFile: existsSync(join(dir, LOCK_FILENAME)) } : modeOptions;
567
+ return blocksToEntities(files, root, hcl2json, opts, sidecar, callers);
483
568
  }
484
569
 
485
570
  /**
package/src/hcl/roots.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  import { existsSync } from "node:fs";
30
30
  import { isAbsolute, resolve } from "node:path";
31
31
  import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
32
- import type { Hcl2Json } from "@intentius/chant/terraform/parse";
32
+ import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
33
33
  import type { TerraformRootConfig } from "../config";
34
34
  import { LIVE_TYPE, parseTerraformRootDir } from "./parse";
35
35
  import { descendModules, resolveCallModuleType, type CallModuleType } from "./descend";