@intentius/chant-lexicon-terraform 0.57.0 → 0.58.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 (381) hide show
  1. package/dist/composites/terraform-adopt-op.d.ts +126 -0
  2. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  3. package/dist/composites/terraform-apply-op.d.ts +63 -16
  4. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  5. package/dist/composites/terraform-watch-op.d.ts +80 -36
  6. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  7. package/dist/config.d.ts +49 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/describe-resources.d.ts +256 -16
  10. package/dist/describe-resources.d.ts.map +1 -1
  11. package/dist/hcl/descend.d.ts +98 -0
  12. package/dist/hcl/descend.d.ts.map +1 -0
  13. package/dist/hcl/marker.d.ts +130 -0
  14. package/dist/hcl/marker.d.ts.map +1 -0
  15. package/dist/hcl/parse.d.ts +142 -9
  16. package/dist/hcl/parse.d.ts.map +1 -1
  17. package/dist/hcl/references.d.ts +76 -0
  18. package/dist/hcl/references.d.ts.map +1 -0
  19. package/dist/hcl/roots.d.ts +22 -3
  20. package/dist/hcl/roots.d.ts.map +1 -1
  21. package/dist/hcl/suppressions.d.ts +54 -0
  22. package/dist/hcl/suppressions.d.ts.map +1 -0
  23. package/dist/hcl/value.d.ts +77 -0
  24. package/dist/hcl/value.d.ts.map +1 -0
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +32 -5
  28. package/dist/lint/audit-catalog.d.ts +3 -2
  29. package/dist/lint/audit-catalog.d.ts.map +1 -1
  30. package/dist/lint/audit-lineage.d.ts +68 -0
  31. package/dist/lint/audit-lineage.d.ts.map +1 -1
  32. package/dist/lint/post-synth/blocks.d.ts +53 -0
  33. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  34. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  36. package/dist/lint/post-synth/index.d.ts.map +1 -1
  37. package/dist/lint/post-synth/module-source.d.ts +38 -0
  38. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  39. package/dist/lint/post-synth/scope.d.ts +65 -0
  40. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  41. package/dist/lint/post-synth/tf001.d.ts +18 -0
  42. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  43. package/dist/lint/post-synth/tf002.d.ts +32 -0
  44. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  45. package/dist/lint/post-synth/tf003.d.ts +22 -0
  46. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  47. package/dist/lint/post-synth/tf004.d.ts +22 -0
  48. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf005.d.ts +28 -0
  50. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  51. package/dist/lint/post-synth/tf006.d.ts +21 -0
  52. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  53. package/dist/lint/post-synth/tf007.d.ts +21 -0
  54. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  55. package/dist/lint/post-synth/tf008.d.ts +22 -0
  56. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  57. package/dist/lint/post-synth/tf009.d.ts +24 -0
  58. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  59. package/dist/lint/post-synth/tf010.d.ts +19 -0
  60. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  61. package/dist/lint/post-synth/tf011.d.ts +18 -0
  62. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  63. package/dist/lint/post-synth/tf012.d.ts +17 -0
  64. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  65. package/dist/lint/post-synth/tf013.d.ts +21 -0
  66. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  67. package/dist/lint/post-synth/tf014.d.ts +25 -0
  68. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  69. package/dist/lint/post-synth/tf015.d.ts +21 -0
  70. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  71. package/dist/lint/post-synth/tf016.d.ts +30 -0
  72. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  73. package/dist/lint/post-synth/tf017.d.ts +19 -0
  74. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  75. package/dist/lint/post-synth/tf018.d.ts +20 -0
  76. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  77. package/dist/lint/post-synth/tf019.d.ts +25 -0
  78. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  79. package/dist/lint/post-synth/tf020.d.ts +38 -0
  80. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  81. package/dist/lint/post-synth/tf021.d.ts +41 -0
  82. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  83. package/dist/lint/post-synth/tf022.d.ts +24 -0
  84. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  85. package/dist/lint/post-synth/tf024.d.ts +24 -0
  86. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  87. package/dist/lint/post-synth/tf025.d.ts +29 -0
  88. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  89. package/dist/lint/post-synth/tf026.d.ts +21 -0
  90. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  91. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  92. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  93. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  94. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  95. package/dist/lint/secret-shape.d.ts +68 -0
  96. package/dist/lint/secret-shape.d.ts.map +1 -0
  97. package/dist/live-ownership.d.ts +55 -0
  98. package/dist/live-ownership.d.ts.map +1 -0
  99. package/dist/lsp/option-keys.d.ts.map +1 -1
  100. package/dist/manifest.json +1 -1
  101. package/dist/okf/index.md +24 -0
  102. package/dist/okf/rules/TF002.md +11 -0
  103. package/dist/okf/rules/TF003.md +11 -0
  104. package/dist/okf/rules/TF004.md +11 -0
  105. package/dist/okf/rules/TF005.md +11 -0
  106. package/dist/okf/rules/TF006.md +11 -0
  107. package/dist/okf/rules/TF007.md +11 -0
  108. package/dist/okf/rules/TF008.md +11 -0
  109. package/dist/okf/rules/TF009.md +11 -0
  110. package/dist/okf/rules/TF010.md +11 -0
  111. package/dist/okf/rules/TF011.md +11 -0
  112. package/dist/okf/rules/TF012.md +11 -0
  113. package/dist/okf/rules/TF013.md +11 -0
  114. package/dist/okf/rules/TF014.md +11 -0
  115. package/dist/okf/rules/TF015.md +11 -0
  116. package/dist/okf/rules/TF016.md +11 -0
  117. package/dist/okf/rules/TF017.md +11 -0
  118. package/dist/okf/rules/TF018.md +11 -0
  119. package/dist/okf/rules/TF019.md +11 -0
  120. package/dist/okf/rules/TF020.md +11 -0
  121. package/dist/okf/rules/TF021.md +11 -0
  122. package/dist/okf/rules/TF022.md +11 -0
  123. package/dist/okf/rules/TF024.md +11 -0
  124. package/dist/okf/rules/TF025.md +11 -0
  125. package/dist/okf/rules/TF026.md +11 -0
  126. package/dist/op/activities/index.d.ts +13 -6
  127. package/dist/op/activities/index.d.ts.map +1 -1
  128. package/dist/op/activities/live-detect.d.ts +51 -0
  129. package/dist/op/activities/live-detect.d.ts.map +1 -0
  130. package/dist/op/activities/terraform.d.ts +445 -17
  131. package/dist/op/activities/terraform.d.ts.map +1 -1
  132. package/dist/op/activity-contracts.d.ts +148 -0
  133. package/dist/op/activity-contracts.d.ts.map +1 -0
  134. package/dist/op/adoption.d.ts +118 -0
  135. package/dist/op/adoption.d.ts.map +1 -0
  136. package/dist/op/builders.d.ts +46 -6
  137. package/dist/op/builders.d.ts.map +1 -1
  138. package/dist/op/resolve-root-mode.d.ts +45 -0
  139. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  140. package/dist/plugin.d.ts.map +1 -1
  141. package/dist/rules/blocks.ts +109 -0
  142. package/dist/rules/module-source.ts +121 -0
  143. package/dist/rules/plan-before-apply.ts +11 -0
  144. package/dist/rules/scope.ts +107 -0
  145. package/dist/rules/tf001.ts +32 -8
  146. package/dist/rules/tf002.ts +151 -0
  147. package/dist/rules/tf003.ts +71 -0
  148. package/dist/rules/tf004.ts +63 -0
  149. package/dist/rules/tf005.ts +76 -0
  150. package/dist/rules/tf006.ts +53 -0
  151. package/dist/rules/tf007.ts +78 -0
  152. package/dist/rules/tf008.ts +75 -0
  153. package/dist/rules/tf009.ts +59 -0
  154. package/dist/rules/tf010.ts +51 -0
  155. package/dist/rules/tf011.ts +49 -0
  156. package/dist/rules/tf012.ts +47 -0
  157. package/dist/rules/tf013.ts +63 -0
  158. package/dist/rules/tf014.ts +72 -0
  159. package/dist/rules/tf015.ts +63 -0
  160. package/dist/rules/tf016.ts +75 -0
  161. package/dist/rules/tf017.ts +50 -0
  162. package/dist/rules/tf018.ts +72 -0
  163. package/dist/rules/tf019.ts +72 -0
  164. package/dist/rules/tf020.ts +123 -0
  165. package/dist/rules/tf021.ts +126 -0
  166. package/dist/rules/tf022.ts +61 -0
  167. package/dist/rules/tf024.ts +70 -0
  168. package/dist/rules/tf025.ts +101 -0
  169. package/dist/rules/tf026.ts +80 -0
  170. package/dist/skills/chant-terraform.md +1 -1
  171. package/package.json +7 -2
  172. package/src/__fixtures__/live/main.tf +28 -0
  173. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  174. package/src/__fixtures__/live-adopt/main.tf +21 -0
  175. package/src/__fixtures__/live-estate/README.md +58 -0
  176. package/src/__fixtures__/live-estate/compute.tf +10 -0
  177. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  178. package/src/__fixtures__/live-estate/logs.tf +8 -0
  179. package/src/__fixtures__/live-estate/network.tf +24 -0
  180. package/src/__fixtures__/live-estate/rule.tf +11 -0
  181. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  182. package/src/__fixtures__/live-estate/versions.tf +31 -0
  183. package/src/__fixtures__/live-ls.json +98 -0
  184. package/src/__fixtures__/live-plan.json +285 -0
  185. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  186. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  187. package/src/__fixtures__/module-tree/README.md +16 -0
  188. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  189. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  190. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  191. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  192. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  193. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  194. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  195. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  196. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  197. package/src/__fixtures__/references/main.tf +56 -0
  198. package/src/codegen/lint-rules-page.test.ts +28 -0
  199. package/src/composites/terraform-adopt-op.acceptance.test.ts +166 -0
  200. package/src/composites/terraform-adopt-op.test.ts +170 -0
  201. package/src/composites/terraform-adopt-op.ts +197 -0
  202. package/src/composites/terraform-apply-op.acceptance.test.ts +183 -5
  203. package/src/composites/terraform-apply-op.test.ts +167 -15
  204. package/src/composites/terraform-apply-op.ts +105 -28
  205. package/src/composites/terraform-watch-op.test.ts +85 -26
  206. package/src/composites/terraform-watch-op.ts +130 -73
  207. package/src/config.ts +32 -3
  208. package/src/describe-resources.live.test.ts +598 -0
  209. package/src/describe-resources.test.ts +53 -0
  210. package/src/describe-resources.ts +811 -28
  211. package/src/hcl/descend.test.ts +162 -0
  212. package/src/hcl/descend.ts +273 -0
  213. package/src/hcl/marker.test.ts +147 -0
  214. package/src/hcl/marker.ts +235 -0
  215. package/src/hcl/parse.test.ts +69 -0
  216. package/src/hcl/parse.ts +249 -27
  217. package/src/hcl/references.test.ts +116 -0
  218. package/src/hcl/references.ts +172 -0
  219. package/src/hcl/roots.ts +59 -5
  220. package/src/hcl/suppressions.test.ts +147 -0
  221. package/src/hcl/suppressions.ts +204 -0
  222. package/src/hcl/value.test.ts +73 -0
  223. package/src/hcl/value.ts +106 -0
  224. package/src/index.ts +31 -1
  225. package/src/lint/audit-catalog.ts +264 -3
  226. package/src/lint/audit-lineage.ts +319 -1
  227. package/src/lint/audit.test.ts +40 -1
  228. package/src/lint/post-synth/blocks.ts +109 -0
  229. package/src/lint/post-synth/fixtures/TF001/negative.tf +28 -0
  230. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  231. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  232. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  233. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  234. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  235. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  236. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  237. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  238. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  239. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  240. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  241. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  242. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  243. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  244. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  245. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  246. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  247. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  248. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  249. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  250. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  251. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  252. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  253. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  254. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  255. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  256. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  257. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  258. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  259. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  260. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  261. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  262. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  263. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  264. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  265. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  266. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  267. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  268. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  269. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  270. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  271. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  272. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  273. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  274. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  275. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  276. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  277. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  278. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  279. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  280. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  281. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  282. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  283. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  284. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  285. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  286. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  287. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  288. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  289. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  290. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  291. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  292. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  293. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  294. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  295. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  296. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  297. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  298. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  299. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  300. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  301. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  302. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  303. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  304. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  305. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  306. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  307. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  309. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  310. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  311. package/src/lint/post-synth/fixtures/load.ts +108 -0
  312. package/src/lint/post-synth/index.ts +48 -0
  313. package/src/lint/post-synth/module-rules.test.ts +153 -0
  314. package/src/lint/post-synth/module-source.test.ts +75 -0
  315. package/src/lint/post-synth/module-source.ts +121 -0
  316. package/src/lint/post-synth/post-synth.test.ts +14 -15
  317. package/src/lint/post-synth/scope.ts +107 -0
  318. package/src/lint/post-synth/single-block.test.ts +263 -0
  319. package/src/lint/post-synth/suppression.test.ts +110 -0
  320. package/src/lint/post-synth/tf001.ts +32 -8
  321. package/src/lint/post-synth/tf002.test.ts +93 -0
  322. package/src/lint/post-synth/tf002.ts +151 -0
  323. package/src/lint/post-synth/tf003.test.ts +75 -0
  324. package/src/lint/post-synth/tf003.ts +71 -0
  325. package/src/lint/post-synth/tf004.test.ts +47 -0
  326. package/src/lint/post-synth/tf004.ts +63 -0
  327. package/src/lint/post-synth/tf005.test.ts +57 -0
  328. package/src/lint/post-synth/tf005.ts +76 -0
  329. package/src/lint/post-synth/tf006.ts +53 -0
  330. package/src/lint/post-synth/tf007.ts +78 -0
  331. package/src/lint/post-synth/tf008.ts +75 -0
  332. package/src/lint/post-synth/tf009.ts +59 -0
  333. package/src/lint/post-synth/tf010.ts +51 -0
  334. package/src/lint/post-synth/tf011.ts +49 -0
  335. package/src/lint/post-synth/tf012.ts +47 -0
  336. package/src/lint/post-synth/tf013.ts +63 -0
  337. package/src/lint/post-synth/tf014.ts +72 -0
  338. package/src/lint/post-synth/tf015.ts +63 -0
  339. package/src/lint/post-synth/tf016.ts +75 -0
  340. package/src/lint/post-synth/tf017.ts +50 -0
  341. package/src/lint/post-synth/tf018.ts +72 -0
  342. package/src/lint/post-synth/tf019.ts +72 -0
  343. package/src/lint/post-synth/tf020.ts +123 -0
  344. package/src/lint/post-synth/tf021.ts +126 -0
  345. package/src/lint/post-synth/tf022.ts +61 -0
  346. package/src/lint/post-synth/tf024.test.ts +42 -0
  347. package/src/lint/post-synth/tf024.ts +70 -0
  348. package/src/lint/post-synth/tf025.test.ts +46 -0
  349. package/src/lint/post-synth/tf025.ts +101 -0
  350. package/src/lint/post-synth/tf026.test.ts +56 -0
  351. package/src/lint/post-synth/tf026.ts +80 -0
  352. package/src/lint/presets.test.ts +72 -0
  353. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  354. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  355. package/src/lint/rules/fixtures/load.ts +53 -0
  356. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  357. package/src/lint/rules/plan-before-apply.ts +11 -0
  358. package/src/lint/secret-shape.test.ts +150 -0
  359. package/src/lint/secret-shape.ts +159 -0
  360. package/src/live-ownership.ts +60 -0
  361. package/src/lsp/completions.test.ts +3 -2
  362. package/src/lsp/option-keys.ts +10 -5
  363. package/src/op/activities/choudoufu.acceptance.test.ts +143 -0
  364. package/src/op/activities/choudoufu.test.ts +693 -0
  365. package/src/op/activities/index.ts +40 -4
  366. package/src/op/activities/live-detect.test.ts +143 -0
  367. package/src/op/activities/live-detect.ts +162 -0
  368. package/src/op/activities/registry.test.ts +11 -3
  369. package/src/op/activities/terraform.test.ts +7 -3
  370. package/src/op/activities/terraform.ts +823 -69
  371. package/src/op/activity-contracts.test.ts +147 -0
  372. package/src/op/activity-contracts.ts +178 -0
  373. package/src/op/adoption.test.ts +170 -0
  374. package/src/op/adoption.ts +241 -0
  375. package/src/op/builders.test.ts +60 -5
  376. package/src/op/builders.ts +93 -5
  377. package/src/op/resolve-root-mode.test.ts +85 -0
  378. package/src/op/resolve-root-mode.ts +72 -0
  379. package/src/plugin.test.ts +68 -0
  380. package/src/plugin.ts +75 -9
  381. package/src/skills/chant-terraform.md +1 -1
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Module descent (chant #2112), against the real fixture tree in
3
+ * `src/__fixtures__/module-tree/`. Every assertion here goes through
4
+ * `renderTerraformRoots`, the same call `buildRoots()` makes, so the keys and
5
+ * warnings under test are the ones a build produces.
6
+ */
7
+
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { describe, expect, it } from "vitest";
11
+ import type { Declarable } from "@intentius/chant/declarable";
12
+ import { renderTerraformRoots } from "./roots";
13
+ import { resolveCallModuleType } from "./descend";
14
+ import { TERRAFORM_TYPE, type TerraformEntity } from "./parse";
15
+
16
+ const TREE = join(dirname(fileURLToPath(import.meta.url)), "..", "__fixtures__", "module-tree");
17
+
18
+ function callersOf(entities: Map<string, Declarable>, key: string): readonly string[] | undefined {
19
+ const entity = entities.get(key);
20
+ return entity ? ((entity as TerraformEntity).props.callers ?? []) : undefined;
21
+ }
22
+
23
+ describe("descending into local modules", () => {
24
+ it("keys a child module's blocks <root>/module.<name>/<address> and records the callers", async () => {
25
+ const { entities } = await renderTerraformRoots({
26
+ projectRoot: TREE,
27
+ roots: { app: { dir: "./root" } },
28
+ });
29
+
30
+ expect(entities.has("app/aws_s3_bucket.root_bucket")).toBe(true);
31
+ expect(entities.has("app/module.cdn/aws_cloudfront_distribution.cdn")).toBe(true);
32
+ expect(callersOf(entities, "app/module.cdn/aws_cloudfront_distribution.cdn")).toEqual(["module.cdn"]);
33
+
34
+ // A root block carries no chain at all.
35
+ expect(callersOf(entities, "app/aws_s3_bucket.root_bucket")).toEqual([]);
36
+ });
37
+
38
+ it("follows a child module's own local calls, one key segment per level", async () => {
39
+ const { entities } = await renderTerraformRoots({
40
+ projectRoot: TREE,
41
+ roots: { app: { dir: "./root" } },
42
+ });
43
+
44
+ const nested = "app/module.cdn/module.bucket/aws_s3_bucket.assets";
45
+ expect(entities.has(nested)).toBe(true);
46
+ expect(callersOf(entities, nested)).toEqual(["module.cdn", "module.bucket"]);
47
+ });
48
+
49
+ it("keeps the root name on a descended entity, so one root stays one root", async () => {
50
+ const { entities } = await renderTerraformRoots({
51
+ projectRoot: TREE,
52
+ roots: { app: { dir: "./root" } },
53
+ });
54
+ const child = entities.get("app/module.cdn/aws_cloudfront_distribution.cdn") as TerraformEntity;
55
+ expect(child.props.root).toBe("app");
56
+ expect(child.props.file).toBe("main.tf");
57
+ });
58
+
59
+ it("does not follow a registry or git source, and says which call site it skipped", async () => {
60
+ const { entities, warnings } = await renderTerraformRoots({
61
+ projectRoot: TREE,
62
+ roots: { app: { dir: "./root" } },
63
+ });
64
+
65
+ expect([...entities.keys()].some((k) => k.startsWith("app/module.vpc/"))).toBe(false);
66
+ const skipped = warnings.find((w) => w.includes("not descending into"));
67
+ expect(skipped).toContain("module.vpc");
68
+ expect(skipped).toContain("terraform-aws-modules/vpc/aws");
69
+ expect(skipped).toContain("reported against the module block at the call site");
70
+ });
71
+
72
+ it('reads nothing but the root itself under "none"', async () => {
73
+ const { entities, warnings } = await renderTerraformRoots({
74
+ projectRoot: TREE,
75
+ roots: { app: { dir: "./root" } },
76
+ callModuleType: "none",
77
+ });
78
+
79
+ expect([...entities.keys()].some((k) => k.includes("/module.cdn/"))).toBe(false);
80
+ expect(entities.has("app/module.cdn")).toBe(true); // the call block itself still parses
81
+ expect(warnings).toEqual([]);
82
+ });
83
+
84
+ it('refuses "all" with a message, and descends into nothing', async () => {
85
+ const { entities, warnings } = await renderTerraformRoots({
86
+ projectRoot: TREE,
87
+ roots: { app: { dir: "./root" } },
88
+ callModuleType: "all",
89
+ });
90
+
91
+ expect(warnings[0]).toContain('terraform.callModuleType: "all" is not supported');
92
+ expect(warnings[0]).toContain("chant fetches nothing");
93
+ expect([...entities.keys()].some((k) => k.includes("/module.cdn/"))).toBe(false);
94
+ });
95
+
96
+ it("refuses a source that resolves outside the project root", async () => {
97
+ const { entities, warnings } = await renderTerraformRoots({
98
+ projectRoot: join(TREE, "outside"),
99
+ roots: { app: { dir: "./root" } },
100
+ });
101
+
102
+ expect([...entities.keys()].some((k) => k.includes("/module.shared/"))).toBe(false);
103
+ const refused = warnings.find((w) => w.includes("outside the project"));
104
+ expect(refused).toContain("module.shared");
105
+ expect(refused).toContain("../../shared");
106
+ });
107
+
108
+ it("stops on a cycle rather than recursing forever", async () => {
109
+ const { entities, warnings } = await renderTerraformRoots({
110
+ projectRoot: TREE,
111
+ roots: { app: { dir: "./cycle" } },
112
+ });
113
+
114
+ expect(entities.has("app/module.a/null_resource.a")).toBe(true);
115
+ expect(entities.has("app/module.a/module.b/null_resource.b")).toBe(true);
116
+ expect(entities.has("app/module.a/module.b/module.a/null_resource.a")).toBe(false);
117
+ expect(warnings.some((w) => w.includes("That is a cycle"))).toBe(true);
118
+ });
119
+
120
+ it("warns, and keeps the rest of the root, when a local source names no directory", async () => {
121
+ const { entities, warnings } = await renderTerraformRoots({
122
+ projectRoot: TREE,
123
+ roots: { app: { dir: "./missing" } },
124
+ });
125
+
126
+ expect(entities.has("app/null_resource.here")).toBe(true);
127
+ expect(warnings.some((w) => w.includes("module.gone") && w.includes("no directory exists"))).toBe(true);
128
+ });
129
+ });
130
+
131
+ describe("resolveCallModuleType", () => {
132
+ it("defaults to local", () => {
133
+ expect(resolveCallModuleType(undefined)).toEqual({ effective: "local" });
134
+ expect(resolveCallModuleType("local")).toEqual({ effective: "local" });
135
+ });
136
+
137
+ it("passes none through with no message", () => {
138
+ expect(resolveCallModuleType("none")).toEqual({ effective: "none" });
139
+ });
140
+
141
+ it("refuses all, and falls back to reading nothing rather than to local", () => {
142
+ const resolved = resolveCallModuleType("all");
143
+ expect(resolved.effective).toBe("none");
144
+ expect(resolved.warning).toContain("requires fetching them");
145
+ });
146
+ });
147
+
148
+ describe("the descended entities the rules read", () => {
149
+ it("marks a child module's terraform block as child-scoped, not as the root's", async () => {
150
+ const { entities } = await renderTerraformRoots({
151
+ projectRoot: TREE,
152
+ roots: { app: { dir: "./root" } },
153
+ });
154
+
155
+ const child = entities.get("app/module.cdn/module.bucket/terraform") as TerraformEntity;
156
+ expect(child.entityType).toBe(TERRAFORM_TYPE);
157
+ expect(child.props.callers).toEqual(["module.cdn", "module.bucket"]);
158
+
159
+ const root = entities.get("app/terraform") as TerraformEntity;
160
+ expect(root.props.callers).toBeUndefined();
161
+ });
162
+ });
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Descend into local child modules (chant #2112).
3
+ *
4
+ * `parseTerraformRootDir` reads one directory, non-recursively, which is
5
+ * Terraform's own root-module scoping and the right unit for a root-scoped
6
+ * check like TF001. Four rules need more than that: TF014 and TF015 are about
7
+ * what a CHILD module may contain, and TF020 needs every reference in a scope
8
+ * before it can call a declaration unused. This module supplies the missing
9
+ * half: a `module` block whose `source` is a local path names a directory,
10
+ * and that directory parses with the same `blocksToEntities` into entities
11
+ * keyed `<root>/module.<name>/<address>` carrying `props.callers`.
12
+ *
13
+ * ## What is followed, and what is not
14
+ *
15
+ * tflint's `--call-module-type` (three states, defaulting to `local`,
16
+ * https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/calling-modules.md)
17
+ * is the model:
18
+ *
19
+ * - `local` (the default): only `./` and `../` sources are read. A registry
20
+ * or git source is NOT fetched, so its contents are never linted and every
21
+ * finding about it is reported against the call site instead, which is
22
+ * where TF004 and TF005 already report. The skipped call sites are named in
23
+ * a warning rather than passed over in silence.
24
+ * - `none`: no descent at all. A root then parses exactly as it did before
25
+ * this module existed.
26
+ * - `all`: reserved. tflint means "fetch registry and git modules too", which
27
+ * needs a `terraform init`/`terraform get` first so the modules are on disk
28
+ * under `.terraform`. chant fetches nothing, so this is refused with a
29
+ * message rather than quietly behaving like `local`.
30
+ *
31
+ * Two more refusals, both about staying inside the repository being read:
32
+ *
33
+ * - A source that resolves outside the project root is refused. `../` is a
34
+ * legal Terraform source and a repository may legitimately share modules
35
+ * between sibling roots, but reading above the project root means linting
36
+ * files the project does not own, so the boundary is the project root and
37
+ * crossing it is a warning, not a parse.
38
+ * - A source already on the current call chain is a cycle (`a` calls `b`
39
+ * calls `a`) and stops there. The visited set is per chain, on RESOLVED
40
+ * paths, so one module called from two different places is still read once
41
+ * per call site, each under its own `module.<name>` key, the way
42
+ * `terraform show -json` reports two `child_modules[]` entries for it.
43
+ *
44
+ * Everything here is best effort, like `./roots.ts`: an unreadable or
45
+ * unparseable child module is a warning and no entities, never a throw, since
46
+ * half a root parsed is more useful than none of it.
47
+ */
48
+
49
+ import { existsSync, statSync } from "node:fs";
50
+ import { isAbsolute, relative, resolve } from "node:path";
51
+ import type { Declarable } from "@intentius/chant/declarable";
52
+ import type { Hcl2Json } from "@intentius/chant/terraform/parse";
53
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
54
+ import { classifyModuleSource } from "../lint/post-synth/module-source";
55
+ import {
56
+ MODULE_TYPE,
57
+ parseTerraformRootDir,
58
+ type BlockBody,
59
+ type TerraformEntity,
60
+ type TerraformRootModeOptions,
61
+ } from "./parse";
62
+
63
+ /** tflint's `--call-module-type`, with the same three states and the same default. */
64
+ export type CallModuleType = "local" | "none" | "all";
65
+
66
+ /** What a caller gets when it asks for a mode chant cannot serve. */
67
+ export interface ResolvedCallModuleType {
68
+ /** The mode actually used. `"all"` never survives here. */
69
+ effective: "local" | "none";
70
+ /** Set when the requested mode was refused, ready to push onto a warning list. */
71
+ warning?: string;
72
+ }
73
+
74
+ /**
75
+ * Resolve the configured `callModuleType` into one this lexicon implements.
76
+ * `"all"` is refused with a message naming what it would need; everything
77
+ * else passes through, and an absent value is `"local"`, tflint's default.
78
+ */
79
+ export function resolveCallModuleType(value?: CallModuleType): ResolvedCallModuleType {
80
+ if (value === "none") return { effective: "none" };
81
+ if (value === "all") {
82
+ return {
83
+ effective: "none",
84
+ warning:
85
+ 'terraform.callModuleType: "all" is not supported. It means "descend into registry and git ' +
86
+ "modules too\", which requires fetching them (`terraform init`/`terraform get`) first, and chant " +
87
+ 'fetches nothing. Use "local" (the default, relative-path sources only) or "none". No module was descended into.',
88
+ };
89
+ }
90
+ return { effective: "local" };
91
+ }
92
+
93
+ /** Options for {@link descendModules}. */
94
+ export interface DescendOptions {
95
+ /** Directory the scope's own `.tf` files were read from. */
96
+ dir: string;
97
+ /** Root name, the first segment of every entity key. */
98
+ root: string;
99
+ /** Boundary: a source resolving outside this directory is refused. */
100
+ projectRoot: string;
101
+ /** Resolved mode. `"none"` returns immediately with nothing. */
102
+ callModuleType?: "local" | "none";
103
+ /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktf/hcl2json`. */
104
+ hcl2json?: Hcl2Json;
105
+ /** Mode facts stamped onto the child's entities, exactly as on the root's. */
106
+ modeOptions?: TerraformRootModeOptions;
107
+ /** How deep to follow calls. A guard against a pathological tree, not a policy. */
108
+ maxDepth?: number;
109
+ }
110
+
111
+ export interface DescendResult {
112
+ /** The child modules' entities, keyed `<root>/module.<name>/<address>`. */
113
+ entities: Map<string, Declarable>;
114
+ /** One line per refusal or skip, in the order they were met. */
115
+ warnings: string[];
116
+ }
117
+
118
+ const DEFAULT_MAX_DEPTH = 8;
119
+
120
+ /** Is `path` inside `boundary` (or the boundary itself)? */
121
+ function isInside(boundary: string, path: string): boolean {
122
+ const rel = relative(boundary, path);
123
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
124
+ }
125
+
126
+ /** The `module` blocks of one scope, in `ctx.entities` order, with their sources read. */
127
+ function moduleCallsOf(entities: Map<string, Declarable>): Array<{ address: string; source?: string }> {
128
+ const calls: Array<{ address: string; source?: string }> = [];
129
+ for (const entity of entities.values()) {
130
+ if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
131
+ const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
132
+ const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
133
+ calls.push({
134
+ address: typeof props.address === "string" ? props.address : "module.?",
135
+ source: typeof body.source === "string" ? body.source : undefined,
136
+ });
137
+ }
138
+ return calls;
139
+ }
140
+
141
+ /**
142
+ * The mode and estate the scope's own entities carry.
143
+ *
144
+ * A child module directory holds no `live` block and no `estate.chdf.hcl`
145
+ * sidecar (choudoufu reads the estate declaration from the root's directory,
146
+ * and only there), so parsing it alone always lands on `mode: "state"`. The
147
+ * root's verdict is the root's, one root one mode, so it is copied down onto
148
+ * every descended entity rather than re-derived from files that cannot carry
149
+ * it.
150
+ */
151
+ function scopeMode(entities: Map<string, Declarable>): { mode?: string; estate?: string } {
152
+ for (const entity of entities.values()) {
153
+ if (!isResourceDeclarable(entity)) continue;
154
+ const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
155
+ if (typeof props.mode !== "string") continue;
156
+ return { mode: props.mode, ...(typeof props.estate === "string" ? { estate: props.estate } : {}) };
157
+ }
158
+ return {};
159
+ }
160
+
161
+ /** Rebuild `entity` with the parent scope's mode and estate on its props. */
162
+ function withMode(entity: Declarable, mode: { mode?: string; estate?: string }): Declarable {
163
+ if (mode.mode === undefined || !isResourceDeclarable(entity)) return entity;
164
+ const te = entity as TerraformEntity;
165
+ return {
166
+ ...te,
167
+ props: { ...te.props, mode: mode.mode as TerraformEntity["props"]["mode"], ...(mode.estate !== undefined ? { estate: mode.estate } : {}) },
168
+ } as Declarable;
169
+ }
170
+
171
+ /**
172
+ * Parse every local child module reachable from `scope`, recursively.
173
+ *
174
+ * `scope` is one already-parsed module scope: the root's own entities on the
175
+ * first call, a child's on a recursive one. Returned entities are the
176
+ * children's only, never the scope's own, so a caller merges them into the
177
+ * map it already has.
178
+ */
179
+ export async function descendModules(
180
+ scope: Map<string, Declarable>,
181
+ opts: DescendOptions,
182
+ ): Promise<DescendResult> {
183
+ const entities = new Map<string, Declarable>();
184
+ const warnings: string[] = [];
185
+ const mode = opts.callModuleType ?? "local";
186
+ if (mode === "none") return { entities, warnings };
187
+
188
+ const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
189
+ const where = `terraform.roots.${opts.root}`;
190
+ // The project root is the boundary, except for a root module configured
191
+ // with a `dir` outside the project entirely (an absolute path to an estate
192
+ // kept elsewhere): there the root's own directory is the boundary, since
193
+ // the project root would refuse every module the root has.
194
+ const boundary = isInside(opts.projectRoot, opts.dir) ? resolve(opts.projectRoot) : resolve(opts.dir);
195
+
196
+ /** One scope's calls, with the chain that reached it and the paths already on that chain. */
197
+ const walk = async (
198
+ scopeEntities: Map<string, Declarable>,
199
+ dir: string,
200
+ callers: readonly string[],
201
+ onChain: readonly string[],
202
+ ): Promise<void> => {
203
+ const notFollowed: string[] = [];
204
+
205
+ for (const call of moduleCallsOf(scopeEntities)) {
206
+ if (call.source === undefined) continue;
207
+ const classified = classifyModuleSource(call.source);
208
+ if (classified.kind !== "local") {
209
+ notFollowed.push(`${call.address} (${call.source})`);
210
+ continue;
211
+ }
212
+
213
+ const target = resolve(dir, call.source);
214
+ if (!isInside(boundary, target)) {
215
+ warnings.push(
216
+ `${where}: ${call.address} sources ${JSON.stringify(call.source)}, which resolves outside the project ` +
217
+ `root (${target}). chant does not read modules from outside the project it was pointed at, so that ` +
218
+ "module is not parsed and any finding in it is not reported.",
219
+ );
220
+ continue;
221
+ }
222
+ if (onChain.includes(target)) {
223
+ warnings.push(
224
+ `${where}: ${call.address} sources ${JSON.stringify(call.source)}, which is already on this call chain ` +
225
+ `(${[...callers, call.address].join(" -> ")}). That is a cycle, so the descent stops here.`,
226
+ );
227
+ continue;
228
+ }
229
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
230
+ warnings.push(
231
+ `${where}: ${call.address} sources ${JSON.stringify(call.source)}, but no directory exists at ${target}, ` +
232
+ "so the module is not parsed.",
233
+ );
234
+ continue;
235
+ }
236
+ if (callers.length >= maxDepth) {
237
+ warnings.push(
238
+ `${where}: ${call.address} is more than ${maxDepth} module calls deep (${callers.join(" -> ")}), ` +
239
+ "so the descent stops here.",
240
+ );
241
+ continue;
242
+ }
243
+
244
+ const chain = [...callers, call.address];
245
+ let child: Map<string, Declarable>;
246
+ try {
247
+ child = await parseTerraformRootDir(target, opts.root, opts.hcl2json, opts.modeOptions, chain);
248
+ } catch (err) {
249
+ const message = err instanceof Error ? err.message : String(err);
250
+ warnings.push(`${where}: could not parse ${call.address} at ${target}, ${message}`);
251
+ continue;
252
+ }
253
+ const mode = scopeMode(scopeEntities);
254
+ for (const [key, entity] of child) {
255
+ const stamped = withMode(entity, mode);
256
+ child.set(key, stamped);
257
+ entities.set(key, stamped);
258
+ }
259
+ await walk(child, target, chain, [...onChain, target]);
260
+ }
261
+
262
+ if (notFollowed.length > 0) {
263
+ warnings.push(
264
+ `${where}: not descending into ${notFollowed.join(", ")}. The source is a registry or git module and chant ` +
265
+ "fetches nothing, so the module's own contents are unchecked and every finding about it is reported " +
266
+ "against the module block at the call site.",
267
+ );
268
+ }
269
+ };
270
+
271
+ await walk(scope, opts.dir, [], [resolve(opts.dir)]);
272
+ return { entities, warnings };
273
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The marker escaping rule, held to choudoufu's own spec (#2104).
3
+ *
4
+ * Every case in the first two `it.each` tables is a row copied verbatim from
5
+ * `live/MARKERS.md` in https://github.com/INTENTIUS/choudoufu, spec version 1:
6
+ * the "Escaping rule" table and the "for_each key escaping" table. They are
7
+ * the contract, so they are the test, and a change to either side shows up
8
+ * here rather than in a live estate.
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ addressMatches,
14
+ escapeAddress,
15
+ escapeAddressVariants,
16
+ escapeForEachKey,
17
+ joinMarkerAddress,
18
+ readMarker,
19
+ MARKER_TAG_ADDRESS,
20
+ MARKER_TAG_ESTATE,
21
+ MARKER_TAG_SLOT,
22
+ } from "./marker";
23
+
24
+ describe("escapeAddress: live/MARKERS.md's own Escaping rule table", () => {
25
+ it.each([
26
+ ["aws_vpc.this", "aws_vpc.this"],
27
+ ['aws_subnet.this["a"]', "aws_subnet.this:a"],
28
+ ["aws_eip.this[2]", "aws_eip.this:2"],
29
+ ['module.subnets["a"].aws_subnet.this', "module.subnets:a.aws_subnet.this"],
30
+ ["module.subnets[2].aws_subnet.this", "module.subnets:2.aws_subnet.this"],
31
+ ['aws_subnet.this["alice.smith"]', "aws_subnet.this:alice@dsmith"],
32
+ ['aws_subnet.this["at@sign"]', "aws_subnet.this:at@@sign"],
33
+ ])("%s escapes to %s", (unescaped, escaped) => {
34
+ expect(escapeAddress(unescaped)).toBe(escaped);
35
+ });
36
+
37
+ it("leaves no AWS-forbidden character in the value", () => {
38
+ for (const address of ['aws_subnet.this["a.b:c@d"]', "module.m[3].aws_eip.pool[10]"]) {
39
+ expect(escapeAddress(address)).not.toMatch(/[[\]"]/);
40
+ }
41
+ });
42
+ });
43
+
44
+ describe("escapeForEachKey: live/MARKERS.md's own for_each key escaping table", () => {
45
+ it.each([
46
+ ["a(b)", "a+000028b+000029"],
47
+ ["plus+one", "plus++one"],
48
+ ["a;b", "a+00003Bb"],
49
+ ])("%s escapes to %s", (raw, escaped) => {
50
+ expect(escapeForEachKey(raw)).toBe(escaped);
51
+ });
52
+
53
+ it("doubles @, then . , then : , in that order", () => {
54
+ expect(escapeForEachKey("at@sign")).toBe("at@@sign");
55
+ expect(escapeForEachKey("alice.smith")).toBe("alice@dsmith");
56
+ expect(escapeForEachKey("a:b")).toBe("a@cb");
57
+ // The order is load-bearing: the `@` the `.` rule introduces must not be
58
+ // doubled again by the `@` rule.
59
+ expect(escapeForEachKey("a.b")).toBe("a@db");
60
+ });
61
+
62
+ it("runs the out-of-charset layer before the doubling", () => {
63
+ // `(` leaves as `+000028`, which contains no @, . or : for the second
64
+ // layer to touch, and the key's own `.` is still doubled.
65
+ expect(escapeForEachKey("a(b).c")).toBe("a+000028b+000029@dc");
66
+ });
67
+
68
+ it("leaves a key already inside the AWS-legal set alone, `+` excepted", () => {
69
+ expect(escapeForEachKey("us-east-1a")).toBe("us-east-1a");
70
+ expect(escapeForEachKey("10.0.0.0/16")).toBe("10@d0@d0@d0/16");
71
+ });
72
+ });
73
+
74
+ describe("addressMatches: escape the declared address, compare strings", () => {
75
+ it("matches the current escaping", () => {
76
+ expect(addressMatches('aws_subnet.this["a"]', "aws_subnet.this:a")).toBe(true);
77
+ });
78
+
79
+ it("matches a marker stamped before the out-of-charset layer landed", () => {
80
+ // `a.b+c` stamped as `a@db+c` in the window between choudoufu #178 and
81
+ // #210: the `.` doubled, the `+` untouched.
82
+ expect(addressMatches('aws_subnet.this["a.b+c"]', "aws_subnet.this:a@db+c")).toBe(true);
83
+ expect(escapeAddress('aws_subnet.this["a.b+c"]')).toBe("aws_subnet.this:a@db++c");
84
+ });
85
+
86
+ it("matches a marker stamped before any key escaping at all", () => {
87
+ expect(addressMatches('aws_subnet.this["at@sign"]', "aws_subnet.this:at@sign")).toBe(true);
88
+ });
89
+
90
+ it("collapses the three escapings to one for an address with no for_each key", () => {
91
+ expect(escapeAddressVariants("aws_vpc.this")).toEqual(["aws_vpc.this"]);
92
+ });
93
+
94
+ it("refuses a value that is not any escaping of the declared address", () => {
95
+ expect(addressMatches("aws_vpc.this", "aws_vpc.other")).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe("joinMarkerAddress: the continuation chain", () => {
100
+ it("returns the head alone when no continuation is present", () => {
101
+ expect(joinMarkerAddress({ [MARKER_TAG_ADDRESS]: "aws_vpc.this" })).toBe("aws_vpc.this");
102
+ });
103
+
104
+ it("concatenates tofu-address, then -2, then -3, then -4, in order", () => {
105
+ expect(
106
+ joinMarkerAddress({
107
+ [MARKER_TAG_ADDRESS]: "aaa",
108
+ "tofu-address-2": "bbb",
109
+ "tofu-address-3": "ccc",
110
+ "tofu-address-4": "ddd",
111
+ }),
112
+ ).toBe("aaabbbcccddd");
113
+ });
114
+
115
+ it("reads a chain with a gap as unreadable, never as the address up to the gap", () => {
116
+ expect(joinMarkerAddress({ [MARKER_TAG_ADDRESS]: "aaa", "tofu-address-3": "ccc" })).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe("readMarker: live/MARKERS.md's Ownership semantics", () => {
121
+ it("a tofu-estate value is the whole ownership claim", () => {
122
+ expect(
123
+ readMarker({ [MARKER_TAG_ESTATE]: "prod", [MARKER_TAG_ADDRESS]: "aws_vpc.this" }),
124
+ ).toEqual({ kind: "owned", estate: "prod", address: "aws_vpc.this" });
125
+ });
126
+
127
+ it("carries the slot when a count instance has one", () => {
128
+ expect(
129
+ readMarker({
130
+ [MARKER_TAG_ESTATE]: "prod",
131
+ [MARKER_TAG_ADDRESS]: "aws_eip.pool:1",
132
+ [MARKER_TAG_SLOT]: "1",
133
+ }),
134
+ ).toEqual({ kind: "owned", estate: "prod", address: "aws_eip.pool:1", slot: "1" });
135
+ });
136
+
137
+ it("neither key present is foreign", () => {
138
+ expect(readMarker({ Name: "someone else's" })).toEqual({ kind: "foreign" });
139
+ expect(readMarker(undefined)).toEqual({ kind: "foreign" });
140
+ });
141
+
142
+ it("an estate with no readable address is malformed, not foreign and not owned", () => {
143
+ const verdict = readMarker({ [MARKER_TAG_ESTATE]: "prod" });
144
+ expect(verdict.kind).toBe("malformed");
145
+ expect(verdict).toMatchObject({ estate: "prod" });
146
+ });
147
+ });