@intentius/chant-lexicon-terraform 0.57.0 → 0.59.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 (408) hide show
  1. package/README.md +9 -0
  2. package/dist/composites/terraform-adopt-op.d.ts +131 -0
  3. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  4. package/dist/composites/terraform-apply-op.d.ts +70 -17
  5. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  6. package/dist/composites/terraform-watch-op.d.ts +104 -37
  7. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  8. package/dist/config.d.ts +49 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/describe-resources.d.ts +256 -16
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/hcl/descend.d.ts +98 -0
  13. package/dist/hcl/descend.d.ts.map +1 -0
  14. package/dist/hcl/marker.d.ts +130 -0
  15. package/dist/hcl/marker.d.ts.map +1 -0
  16. package/dist/hcl/parse.d.ts +156 -9
  17. package/dist/hcl/parse.d.ts.map +1 -1
  18. package/dist/hcl/references.d.ts +76 -0
  19. package/dist/hcl/references.d.ts.map +1 -0
  20. package/dist/hcl/roots.d.ts +22 -3
  21. package/dist/hcl/roots.d.ts.map +1 -1
  22. package/dist/hcl/suppressions.d.ts +54 -0
  23. package/dist/hcl/suppressions.d.ts.map +1 -0
  24. package/dist/hcl/value.d.ts +77 -0
  25. package/dist/hcl/value.d.ts.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/integrity.json +34 -5
  29. package/dist/lint/audit-catalog.d.ts +3 -2
  30. package/dist/lint/audit-catalog.d.ts.map +1 -1
  31. package/dist/lint/audit-lineage.d.ts +95 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -1
  33. package/dist/lint/post-synth/blocks.d.ts +53 -0
  34. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  36. package/dist/lint/post-synth/fixtures/load.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 +38 -0
  39. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  40. package/dist/lint/post-synth/scope.d.ts +65 -0
  41. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  42. package/dist/lint/post-synth/tf001.d.ts +58 -6
  43. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  44. package/dist/lint/post-synth/tf002.d.ts +32 -0
  45. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  46. package/dist/lint/post-synth/tf003.d.ts +22 -0
  47. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  48. package/dist/lint/post-synth/tf004.d.ts +22 -0
  49. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  50. package/dist/lint/post-synth/tf005.d.ts +28 -0
  51. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf006.d.ts +21 -0
  53. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  54. package/dist/lint/post-synth/tf007.d.ts +21 -0
  55. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  56. package/dist/lint/post-synth/tf008.d.ts +22 -0
  57. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  58. package/dist/lint/post-synth/tf009.d.ts +24 -0
  59. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  60. package/dist/lint/post-synth/tf010.d.ts +19 -0
  61. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  62. package/dist/lint/post-synth/tf011.d.ts +18 -0
  63. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  64. package/dist/lint/post-synth/tf012.d.ts +17 -0
  65. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  66. package/dist/lint/post-synth/tf013.d.ts +21 -0
  67. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  68. package/dist/lint/post-synth/tf014.d.ts +25 -0
  69. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  70. package/dist/lint/post-synth/tf015.d.ts +30 -0
  71. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  72. package/dist/lint/post-synth/tf016.d.ts +30 -0
  73. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  74. package/dist/lint/post-synth/tf017.d.ts +19 -0
  75. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  76. package/dist/lint/post-synth/tf018.d.ts +20 -0
  77. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  78. package/dist/lint/post-synth/tf019.d.ts +25 -0
  79. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  80. package/dist/lint/post-synth/tf020.d.ts +38 -0
  81. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  82. package/dist/lint/post-synth/tf021.d.ts +41 -0
  83. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  84. package/dist/lint/post-synth/tf022.d.ts +24 -0
  85. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  86. package/dist/lint/post-synth/tf024.d.ts +24 -0
  87. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  88. package/dist/lint/post-synth/tf025.d.ts +29 -0
  89. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  90. package/dist/lint/post-synth/tf026.d.ts +28 -0
  91. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  92. package/dist/lint/post-synth/tf027.d.ts +30 -0
  93. package/dist/lint/post-synth/tf027.d.ts.map +1 -0
  94. package/dist/lint/post-synth/tf028.d.ts +33 -0
  95. package/dist/lint/post-synth/tf028.d.ts.map +1 -0
  96. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  97. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  98. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  99. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  100. package/dist/lint/secret-shape.d.ts +68 -0
  101. package/dist/lint/secret-shape.d.ts.map +1 -0
  102. package/dist/live-ownership.d.ts +55 -0
  103. package/dist/live-ownership.d.ts.map +1 -0
  104. package/dist/lsp/option-keys.d.ts.map +1 -1
  105. package/dist/manifest.json +1 -1
  106. package/dist/okf/index.md +26 -0
  107. package/dist/okf/rules/TF002.md +11 -0
  108. package/dist/okf/rules/TF003.md +11 -0
  109. package/dist/okf/rules/TF004.md +11 -0
  110. package/dist/okf/rules/TF005.md +11 -0
  111. package/dist/okf/rules/TF006.md +11 -0
  112. package/dist/okf/rules/TF007.md +11 -0
  113. package/dist/okf/rules/TF008.md +11 -0
  114. package/dist/okf/rules/TF009.md +11 -0
  115. package/dist/okf/rules/TF010.md +11 -0
  116. package/dist/okf/rules/TF011.md +11 -0
  117. package/dist/okf/rules/TF012.md +11 -0
  118. package/dist/okf/rules/TF013.md +11 -0
  119. package/dist/okf/rules/TF014.md +11 -0
  120. package/dist/okf/rules/TF015.md +11 -0
  121. package/dist/okf/rules/TF016.md +11 -0
  122. package/dist/okf/rules/TF017.md +11 -0
  123. package/dist/okf/rules/TF018.md +11 -0
  124. package/dist/okf/rules/TF019.md +11 -0
  125. package/dist/okf/rules/TF020.md +11 -0
  126. package/dist/okf/rules/TF021.md +11 -0
  127. package/dist/okf/rules/TF022.md +11 -0
  128. package/dist/okf/rules/TF024.md +11 -0
  129. package/dist/okf/rules/TF025.md +11 -0
  130. package/dist/okf/rules/TF026.md +11 -0
  131. package/dist/okf/rules/TF027.md +11 -0
  132. package/dist/okf/rules/TF028.md +11 -0
  133. package/dist/op/activities/index.d.ts +13 -6
  134. package/dist/op/activities/index.d.ts.map +1 -1
  135. package/dist/op/activities/live-detect.d.ts +51 -0
  136. package/dist/op/activities/live-detect.d.ts.map +1 -0
  137. package/dist/op/activities/terraform.d.ts +466 -17
  138. package/dist/op/activities/terraform.d.ts.map +1 -1
  139. package/dist/op/activity-contracts.d.ts +148 -0
  140. package/dist/op/activity-contracts.d.ts.map +1 -0
  141. package/dist/op/adoption.d.ts +118 -0
  142. package/dist/op/adoption.d.ts.map +1 -0
  143. package/dist/op/builders.d.ts +48 -6
  144. package/dist/op/builders.d.ts.map +1 -1
  145. package/dist/op/resolve-root-mode.d.ts +59 -0
  146. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  147. package/dist/plugin.d.ts.map +1 -1
  148. package/dist/rules/blocks.ts +109 -0
  149. package/dist/rules/module-source.ts +121 -0
  150. package/dist/rules/plan-before-apply.ts +11 -0
  151. package/dist/rules/scope.ts +107 -0
  152. package/dist/rules/tf001.ts +98 -18
  153. package/dist/rules/tf002.ts +151 -0
  154. package/dist/rules/tf003.ts +71 -0
  155. package/dist/rules/tf004.ts +63 -0
  156. package/dist/rules/tf005.ts +76 -0
  157. package/dist/rules/tf006.ts +53 -0
  158. package/dist/rules/tf007.ts +78 -0
  159. package/dist/rules/tf008.ts +75 -0
  160. package/dist/rules/tf009.ts +59 -0
  161. package/dist/rules/tf010.ts +51 -0
  162. package/dist/rules/tf011.ts +49 -0
  163. package/dist/rules/tf012.ts +47 -0
  164. package/dist/rules/tf013.ts +63 -0
  165. package/dist/rules/tf014.ts +72 -0
  166. package/dist/rules/tf015.ts +72 -0
  167. package/dist/rules/tf016.ts +75 -0
  168. package/dist/rules/tf017.ts +50 -0
  169. package/dist/rules/tf018.ts +72 -0
  170. package/dist/rules/tf019.ts +72 -0
  171. package/dist/rules/tf020.ts +123 -0
  172. package/dist/rules/tf021.ts +126 -0
  173. package/dist/rules/tf022.ts +61 -0
  174. package/dist/rules/tf024.ts +70 -0
  175. package/dist/rules/tf025.ts +101 -0
  176. package/dist/rules/tf026.ts +84 -0
  177. package/dist/rules/tf027.ts +74 -0
  178. package/dist/rules/tf028.ts +96 -0
  179. package/dist/skills/chant-terraform.md +1 -1
  180. package/dist/validate.d.ts.map +1 -1
  181. package/package.json +7 -2
  182. package/src/__fixtures__/ACCEPTANCE.md +181 -0
  183. package/src/__fixtures__/live/main.tf +28 -0
  184. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  185. package/src/__fixtures__/live-adopt/main.tf +21 -0
  186. package/src/__fixtures__/live-estate/README.md +58 -0
  187. package/src/__fixtures__/live-estate/compute.tf +10 -0
  188. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  189. package/src/__fixtures__/live-estate/logs.tf +8 -0
  190. package/src/__fixtures__/live-estate/network.tf +24 -0
  191. package/src/__fixtures__/live-estate/rule.tf +11 -0
  192. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  193. package/src/__fixtures__/live-estate/versions.tf +31 -0
  194. package/src/__fixtures__/live-ls.json +98 -0
  195. package/src/__fixtures__/live-plan.json +285 -0
  196. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  197. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  198. package/src/__fixtures__/module-tree/README.md +16 -0
  199. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  200. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  201. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  202. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  203. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  204. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  205. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  206. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  207. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  208. package/src/__fixtures__/references/main.tf +56 -0
  209. package/src/__fixtures__/remote-backend/main.tf +34 -0
  210. package/src/__fixtures__/with-backend/main.tf +7 -0
  211. package/src/codegen/lint-rules-page.test.ts +45 -0
  212. package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
  213. package/src/composites/terraform-adopt-op.test.ts +175 -0
  214. package/src/composites/terraform-adopt-op.ts +202 -0
  215. package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
  216. package/src/composites/terraform-apply-op.test.ts +179 -19
  217. package/src/composites/terraform-apply-op.ts +123 -30
  218. package/src/composites/terraform-watch-op.test.ts +169 -29
  219. package/src/composites/terraform-watch-op.ts +183 -75
  220. package/src/config.ts +32 -3
  221. package/src/describe-resources.live.test.ts +598 -0
  222. package/src/describe-resources.test.ts +53 -0
  223. package/src/describe-resources.ts +811 -28
  224. package/src/hcl/descend.test.ts +162 -0
  225. package/src/hcl/descend.ts +273 -0
  226. package/src/hcl/marker.test.ts +147 -0
  227. package/src/hcl/marker.ts +235 -0
  228. package/src/hcl/parse.test.ts +69 -0
  229. package/src/hcl/parse.ts +269 -27
  230. package/src/hcl/references.test.ts +116 -0
  231. package/src/hcl/references.ts +172 -0
  232. package/src/hcl/roots.ts +59 -5
  233. package/src/hcl/suppressions.test.ts +147 -0
  234. package/src/hcl/suppressions.ts +204 -0
  235. package/src/hcl/value.test.ts +73 -0
  236. package/src/hcl/value.ts +106 -0
  237. package/src/index.ts +31 -1
  238. package/src/lint/audit-catalog.ts +288 -3
  239. package/src/lint/audit-lineage.ts +384 -1
  240. package/src/lint/audit.test.ts +210 -3
  241. package/src/lint/post-synth/blocks.ts +109 -0
  242. package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
  243. package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
  244. package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
  245. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  246. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  247. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  248. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  249. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  250. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  251. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  252. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  253. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  254. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  255. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  256. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  257. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  258. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  259. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  260. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  261. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  262. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  263. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  264. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  265. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  266. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  267. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  268. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  269. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  270. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  271. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  272. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  273. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  274. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  275. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  276. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  277. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  278. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  279. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  280. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  281. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  282. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  283. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  284. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  285. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  286. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  287. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  288. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  289. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  290. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  291. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  292. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  293. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  294. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  295. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  296. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  297. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  298. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  299. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  300. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  301. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  302. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  303. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  304. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  305. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  306. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  307. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  309. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  310. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  311. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  312. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  313. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  314. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  315. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  316. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  317. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  318. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  319. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  320. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  321. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  322. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  323. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  324. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  325. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  326. package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
  327. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
  328. package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
  329. package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
  330. package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
  331. package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
  332. package/src/lint/post-synth/fixtures/load.ts +108 -0
  333. package/src/lint/post-synth/index.ts +52 -0
  334. package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
  335. package/src/lint/post-synth/module-rules.test.ts +153 -0
  336. package/src/lint/post-synth/module-source.test.ts +75 -0
  337. package/src/lint/post-synth/module-source.ts +121 -0
  338. package/src/lint/post-synth/post-synth.test.ts +73 -16
  339. package/src/lint/post-synth/scope.ts +107 -0
  340. package/src/lint/post-synth/single-block.test.ts +263 -0
  341. package/src/lint/post-synth/suppression.test.ts +110 -0
  342. package/src/lint/post-synth/tf001.ts +98 -18
  343. package/src/lint/post-synth/tf002.test.ts +93 -0
  344. package/src/lint/post-synth/tf002.ts +151 -0
  345. package/src/lint/post-synth/tf003.test.ts +75 -0
  346. package/src/lint/post-synth/tf003.ts +71 -0
  347. package/src/lint/post-synth/tf004.test.ts +47 -0
  348. package/src/lint/post-synth/tf004.ts +63 -0
  349. package/src/lint/post-synth/tf005.test.ts +57 -0
  350. package/src/lint/post-synth/tf005.ts +76 -0
  351. package/src/lint/post-synth/tf006.ts +53 -0
  352. package/src/lint/post-synth/tf007.ts +78 -0
  353. package/src/lint/post-synth/tf008.ts +75 -0
  354. package/src/lint/post-synth/tf009.ts +59 -0
  355. package/src/lint/post-synth/tf010.ts +51 -0
  356. package/src/lint/post-synth/tf011.ts +49 -0
  357. package/src/lint/post-synth/tf012.ts +47 -0
  358. package/src/lint/post-synth/tf013.ts +63 -0
  359. package/src/lint/post-synth/tf014.ts +72 -0
  360. package/src/lint/post-synth/tf015.ts +72 -0
  361. package/src/lint/post-synth/tf016.ts +75 -0
  362. package/src/lint/post-synth/tf017.ts +50 -0
  363. package/src/lint/post-synth/tf018.ts +72 -0
  364. package/src/lint/post-synth/tf019.ts +72 -0
  365. package/src/lint/post-synth/tf020.ts +123 -0
  366. package/src/lint/post-synth/tf021.ts +126 -0
  367. package/src/lint/post-synth/tf022.ts +61 -0
  368. package/src/lint/post-synth/tf024.test.ts +42 -0
  369. package/src/lint/post-synth/tf024.ts +70 -0
  370. package/src/lint/post-synth/tf025.test.ts +46 -0
  371. package/src/lint/post-synth/tf025.ts +101 -0
  372. package/src/lint/post-synth/tf026.test.ts +56 -0
  373. package/src/lint/post-synth/tf026.ts +84 -0
  374. package/src/lint/post-synth/tf027.test.ts +62 -0
  375. package/src/lint/post-synth/tf027.ts +74 -0
  376. package/src/lint/post-synth/tf028.test.ts +92 -0
  377. package/src/lint/post-synth/tf028.ts +96 -0
  378. package/src/lint/presets.test.ts +72 -0
  379. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  380. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  381. package/src/lint/rules/fixtures/load.ts +53 -0
  382. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  383. package/src/lint/rules/plan-before-apply.ts +11 -0
  384. package/src/lint/secret-shape.test.ts +150 -0
  385. package/src/lint/secret-shape.ts +159 -0
  386. package/src/live-ownership.ts +60 -0
  387. package/src/lsp/completions.test.ts +3 -2
  388. package/src/lsp/option-keys.ts +12 -6
  389. package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
  390. package/src/op/activities/choudoufu.test.ts +725 -0
  391. package/src/op/activities/index.ts +40 -4
  392. package/src/op/activities/live-detect.test.ts +143 -0
  393. package/src/op/activities/live-detect.ts +162 -0
  394. package/src/op/activities/registry.test.ts +11 -3
  395. package/src/op/activities/terraform.test.ts +7 -3
  396. package/src/op/activities/terraform.ts +861 -69
  397. package/src/op/activity-contracts.test.ts +147 -0
  398. package/src/op/activity-contracts.ts +178 -0
  399. package/src/op/adoption.test.ts +170 -0
  400. package/src/op/adoption.ts +241 -0
  401. package/src/op/builders.test.ts +60 -5
  402. package/src/op/builders.ts +95 -5
  403. package/src/op/resolve-root-mode.test.ts +85 -0
  404. package/src/op/resolve-root-mode.ts +86 -0
  405. package/src/plugin.test.ts +106 -0
  406. package/src/plugin.ts +186 -29
  407. package/src/skills/chant-terraform.md +1 -1
  408. package/src/validate.ts +10 -1
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Which declarations does a module scope actually reference (chant #2112)?
3
+ *
4
+ * One pass over every entity's body, collecting the reference forms Terraform
5
+ * writes as expressions: `var.<name>`, `local.<name>`, `data.<type>.<name>`,
6
+ * `module.<name>`, and the `provider = <type>.<alias>` / `providers = { <type>
7
+ * = <type>.<alias> }` meta-arguments. The result is keyed per module scope
8
+ * (`./parse.ts`'s `scopeOfKey`), because Terraform's own namespaces are: a
9
+ * `var.region` inside `module.cdn` is the child module's variable, never the
10
+ * root's, and a rule that pooled the two would call a genuinely unused root
11
+ * variable used.
12
+ *
13
+ * ## It is a string scan, and the rules built on it say so
14
+ *
15
+ * hcl2json gives back an expression as a STRING, with the `${...}` wrapper
16
+ * preserved: `count = length(var.subnets)` arrives as `"${length(var.subnets)}"`
17
+ * and `bucket = var.name` as `"${var.name}"`. There is no expression tree to
18
+ * walk, so this scans the strings with regexes. That reads a reference inside
19
+ * a comment-free body correctly and is deliberately generous at the edges: a
20
+ * name that appears inside a quoted literal (`description = "set var.region"`)
21
+ * counts as a reference, and a reference built dynamically
22
+ * (`lookup(local.by_env, terraform.workspace)` reaching a value nothing names
23
+ * directly) does not. Both directions are why TF020 is report-only and credits
24
+ * tflint's `terraform_unused_declarations` as `overlaps` rather than
25
+ * `equivalent`: it is the same question answered with a coarser instrument.
26
+ *
27
+ * The index records WHICH entity each reference came from, so a check can ask
28
+ * "is anything other than the declaration itself referring to this?". A
29
+ * `variable "port"` whose own `validation` block reads `var.port` is the case
30
+ * that needs it: without the exclusion every validated variable would look
31
+ * used.
32
+ */
33
+
34
+ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
35
+ import { scopeOfKey, type BlockBody, type TerraformEntity } from "./parse";
36
+
37
+ /** An identifier as Terraform's grammar allows it: letters, digits, `_`, `-`. */
38
+ const NAME = "[A-Za-z_][A-Za-z0-9_-]*";
39
+
40
+ const VAR_RE = new RegExp(`\\bvar\\.(${NAME})`, "g");
41
+ const LOCAL_RE = new RegExp(`\\blocal\\.(${NAME})`, "g");
42
+ const DATA_RE = new RegExp(`\\bdata\\.(${NAME})\\.(${NAME})`, "g");
43
+ const MODULE_RE = new RegExp(`\\bmodule\\.(${NAME})`, "g");
44
+ /** A provider reference once any `${...}` wrapper is off: `aws.west`. */
45
+ const PROVIDER_REF_RE = new RegExp(`^(${NAME})\\.(${NAME})$`);
46
+ /** hcl2json's wrapper around a bare expression, which a `provider` value always is. */
47
+ const INTERPOLATION_RE = /^\$\{(.*)\}$/s;
48
+
49
+ /**
50
+ * Every reference token in one string, in the vocabulary the index keys by:
51
+ * `var.region`, `local.tags`, `data.aws_ami.ubuntu`, `module.cdn`.
52
+ *
53
+ * `data.` is matched before `local.`/`var.` can see it, and a `module.cdn.url`
54
+ * yields `module.cdn`, since the call is what a reference makes used.
55
+ */
56
+ export function collectReferences(text: string): string[] {
57
+ const found = new Set<string>();
58
+ for (const m of text.matchAll(DATA_RE)) found.add(`data.${m[1]}.${m[2]}`);
59
+ for (const m of text.matchAll(VAR_RE)) found.add(`var.${m[1]}`);
60
+ for (const m of text.matchAll(LOCAL_RE)) found.add(`local.${m[1]}`);
61
+ for (const m of text.matchAll(MODULE_RE)) found.add(`module.${m[1]}`);
62
+ return [...found];
63
+ }
64
+
65
+ /**
66
+ * The `provider`/`providers` meta-argument's own reference form, which is not
67
+ * an interpolation and has no prefix of its own: `provider = aws.west` names
68
+ * the `provider "aws" { alias = "west" }` block. Returned in the index's
69
+ * vocabulary, `provider.aws.west`, so it cannot collide with a resource
70
+ * address that happens to look the same.
71
+ */
72
+ export function providerReference(value: unknown): string | undefined {
73
+ if (typeof value !== "string") return undefined;
74
+ const trimmed = value.trim();
75
+ const inner = INTERPOLATION_RE.exec(trimmed);
76
+ const m = PROVIDER_REF_RE.exec((inner ? inner[1] : trimmed).trim());
77
+ return m ? `provider.${m[1]}.${m[2]}` : undefined;
78
+ }
79
+
80
+ /** Every reference in one block body, provider meta-arguments included. */
81
+ export function referencesInBody(body: BlockBody): string[] {
82
+ const found = new Set<string>();
83
+
84
+ const visit = (value: unknown, key: string, depth: number): void => {
85
+ if (depth > 6) return;
86
+ if (typeof value === "string") {
87
+ for (const ref of collectReferences(value)) found.add(ref);
88
+ if (key === "provider") {
89
+ const provider = providerReference(value);
90
+ if (provider) found.add(provider);
91
+ }
92
+ return;
93
+ }
94
+ if (Array.isArray(value)) {
95
+ for (const item of value) visit(item, key, depth + 1);
96
+ return;
97
+ }
98
+ if (typeof value === "object" && value !== null) {
99
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
100
+ // `providers = { aws = aws.west }` maps a child module's provider name
101
+ // to the caller's block, so every VALUE under `providers` is a
102
+ // provider reference, whatever its key.
103
+ visit(v, key === "providers" ? "provider" : k, depth + 1);
104
+ }
105
+ }
106
+ };
107
+
108
+ visit(body, "", 0);
109
+ return [...found];
110
+ }
111
+
112
+ /**
113
+ * Every reference in one module scope: token to the entity keys whose body
114
+ * mentions it.
115
+ */
116
+ export type ScopeReferences = Map<string, Set<string>>;
117
+
118
+ /** Reference tokens per module scope (`<root>`, `<root>/module.<name>`). */
119
+ export interface ReferenceIndex {
120
+ readonly scopes: Map<string, ScopeReferences>;
121
+ }
122
+
123
+ /**
124
+ * Build the index over a whole build's entities. Scopes come from the entity
125
+ * keys, so a root and each of its descended child modules are separate, and
126
+ * two roots never pool.
127
+ */
128
+ export function buildReferenceIndex(entities: Map<string, Declarable>): ReferenceIndex {
129
+ const scopes = new Map<string, ScopeReferences>();
130
+
131
+ for (const [key, entity] of entities) {
132
+ if (!isResourceDeclarable(entity)) continue;
133
+ const props = (entity as TerraformEntity).props as Partial<TerraformEntity["props"]>;
134
+ const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
135
+ const scope = scopeOfKey(key);
136
+ let refs = scopes.get(scope);
137
+ if (!refs) {
138
+ refs = new Map();
139
+ scopes.set(scope, refs);
140
+ }
141
+ for (const ref of referencesInBody(body)) {
142
+ const from = refs.get(ref);
143
+ if (from) from.add(key);
144
+ else refs.set(ref, new Set([key]));
145
+ }
146
+ }
147
+
148
+ return { scopes };
149
+ }
150
+
151
+ /** The entity keys in `scope` whose body references `token`. */
152
+ export function referencesTo(index: ReferenceIndex, scope: string, token: string): Set<string> {
153
+ return index.scopes.get(scope)?.get(token) ?? new Set();
154
+ }
155
+
156
+ /**
157
+ * Is `token` referenced anywhere in `scope` other than by `declaredBy` itself?
158
+ * The exclusion is what keeps a variable read only by its own `validation`
159
+ * block, or a local read only by its own `locals` block, from looking used.
160
+ */
161
+ export function isReferenced(
162
+ index: ReferenceIndex,
163
+ scope: string,
164
+ token: string,
165
+ declaredBy?: string,
166
+ ): boolean {
167
+ const from = referencesTo(index, scope, token);
168
+ for (const key of from) {
169
+ if (key !== declaredBy) return true;
170
+ }
171
+ return false;
172
+ }
package/src/hcl/roots.ts CHANGED
@@ -13,14 +13,20 @@
13
13
  * someone else's estate is the whole job here, and half of it parsing is more
14
14
  * useful than none of it, especially when the audit path (#2085) walks
15
15
  * repositories it did not write.
16
+ *
17
+ * A root's own directory is not the end of the parse: a `module` block with a
18
+ * local source is followed into its directory and parsed as a child scope of
19
+ * the root (#2112). `./descend.ts` owns that walk and every refusal in it; the
20
+ * refusals surface here as this render's warnings.
16
21
  */
17
22
 
18
23
  import { existsSync } from "node:fs";
19
24
  import { isAbsolute, resolve } from "node:path";
20
- import type { Declarable } from "@intentius/chant/declarable";
25
+ import { isResourceDeclarable, type Declarable } from "@intentius/chant/declarable";
21
26
  import type { Hcl2Json } from "@intentius/chant/terraform/parse";
22
27
  import type { TerraformRootConfig } from "../config";
23
- import { parseTerraformRootDir } from "./parse";
28
+ import { LIVE_TYPE, parseTerraformRootDir } from "./parse";
29
+ import { descendModules, resolveCallModuleType, type CallModuleType } from "./descend";
24
30
 
25
31
  export interface TerraformRootsResult {
26
32
  entities: Map<string, Declarable>;
@@ -32,13 +38,26 @@ export interface RenderTerraformRootsOptions {
32
38
  projectRoot: string;
33
39
  /** `terraform.roots`: name to root-module config. */
34
40
  roots: Readonly<Record<string, TerraformRootConfig>>;
41
+ /**
42
+ * `terraform.binary`. A root is live only when this is `"choudoufu"` and it
43
+ * declares an estate; anything else runs stock (#2103).
44
+ */
45
+ binary?: string;
46
+ /**
47
+ * `terraform.callModuleType` (#2112): how far each root's parse follows its
48
+ * `module` calls. Defaults to `"local"`, so a module sourced from `./` or
49
+ * `../` is parsed as a child scope of the root that calls it.
50
+ */
51
+ callModuleType?: CallModuleType;
35
52
  /** Injectable parser (tests); defaults to core's lazy-loaded `@cdktf/hcl2json`. */
36
53
  hcl2json?: Hcl2Json;
37
54
  }
38
55
 
39
56
  /**
40
- * Parse each configured root into entities keyed `<root>/<address>`. Roots are
41
- * visited in declaration order, and one root's failure never stops the next.
57
+ * Parse each configured root into entities keyed `<root>/<address>`, plus its
58
+ * local child modules, keyed `<root>/module.<name>/<address>` (#2112). Roots
59
+ * are visited in declaration order, and one root's failure never stops the
60
+ * next.
42
61
  */
43
62
  export async function renderTerraformRoots(
44
63
  opts: RenderTerraformRootsOptions,
@@ -46,6 +65,11 @@ export async function renderTerraformRoots(
46
65
  const entities = new Map<string, Declarable>();
47
66
  const warnings: string[] = [];
48
67
 
68
+ // One verdict for the whole render: the mode is a project-level setting, so
69
+ // a refused `"all"` is said once rather than once per root (#2112).
70
+ const callModuleType = resolveCallModuleType(opts.callModuleType);
71
+ if (callModuleType.warning) warnings.push(callModuleType.warning);
72
+
49
73
  for (const [name, root] of Object.entries(opts.roots)) {
50
74
  const dir = isAbsolute(root.dir) ? root.dir : resolve(opts.projectRoot, root.dir);
51
75
 
@@ -55,8 +79,38 @@ export async function renderTerraformRoots(
55
79
  }
56
80
 
57
81
  try {
58
- const parsed = await parseTerraformRootDir(dir, name, opts.hcl2json);
82
+ const modeOptions = { binary: opts.binary, workspace: root.workspace, delete: root.delete };
83
+ const parsed = await parseTerraformRootDir(dir, name, opts.hcl2json, modeOptions);
59
84
  for (const [key, entity] of parsed) entities.set(key, entity);
85
+
86
+ // Local child modules, keyed `<root>/module.<name>/<address>` (#2112).
87
+ // Their refusals are warnings on the root that called them, since the
88
+ // call site is where a reader can act on one.
89
+ const descended = await descendModules(parsed, {
90
+ dir,
91
+ root: name,
92
+ projectRoot: opts.projectRoot,
93
+ callModuleType: callModuleType.effective,
94
+ hcl2json: opts.hcl2json,
95
+ modeOptions,
96
+ });
97
+ for (const [key, entity] of descended.entities) entities.set(key, entity);
98
+ warnings.push(...descended.warnings);
99
+
100
+ // A `live` block or `estate.chdf.hcl` sidecar only takes effect under
101
+ // choudoufu; under any other binary it parses fine but is inert, which
102
+ // is worth a warning rather than silence (#2103).
103
+ for (const entity of parsed.values()) {
104
+ if (entity.entityType !== LIVE_TYPE || !isResourceDeclarable(entity)) continue;
105
+ if ((entity.props as { mode?: string }).mode === "state") {
106
+ warnings.push(
107
+ `terraform.roots.${name}: declares a live estate (a \`live\` block or ${JSON.stringify(
108
+ "estate.chdf.hcl",
109
+ )} sidecar), but terraform.binary is ${JSON.stringify(opts.binary ?? "terraform")}, not "choudoufu", so the live declaration is inert.`,
110
+ );
111
+ }
112
+ break;
113
+ }
60
114
  } catch (err) {
61
115
  const message = err instanceof Error ? err.message : String(err);
62
116
  warnings.push(`terraform.roots.${name}: could not parse ${dir}, ${message}`);
@@ -0,0 +1,147 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { scanSuppressions, directivesFor } from "./suppressions";
3
+
4
+ describe("scanSuppressions: block start lines", () => {
5
+ test("finds a bare block's start line", () => {
6
+ const scan = scanSuppressions("main.tf", `terraform {\n required_version = ">= 1.5.0"\n}\n`);
7
+ expect(scan.blockLineQueues.get("terraform")).toEqual([1]);
8
+ });
9
+
10
+ test("finds a one-label block's start line and address", () => {
11
+ const scan = scanSuppressions("main.tf", `variable "region" {\n type = string\n}\n`);
12
+ expect(scan.blockLineQueues.get("var.region")).toEqual([1]);
13
+ });
14
+
15
+ test("finds a two-label block's start line and address", () => {
16
+ const scan = scanSuppressions(
17
+ "main.tf",
18
+ `resource "aws_s3_bucket" "assets" {\n bucket = "x"\n}\n`,
19
+ );
20
+ expect(scan.blockLineQueues.get("aws_s3_bucket.assets")).toEqual([1]);
21
+ });
22
+
23
+ test("a data block's address is prefixed with data.", () => {
24
+ const scan = scanSuppressions("main.tf", `data "aws_ami" "x" {\n most_recent = true\n}\n`);
25
+ expect(scan.blockLineQueues.get("data.aws_ami.x")).toEqual([1]);
26
+ });
27
+
28
+ test("ignores a nested block of the same keyword shape (brace depth)", () => {
29
+ // `required_providers` isn't itself a tracked keyword, but its nested
30
+ // `aws = { ... }` map must not confuse the depth counter into missing the
31
+ // close of the outer `terraform` block.
32
+ const scan = scanSuppressions(
33
+ "main.tf",
34
+ [
35
+ "terraform {",
36
+ " required_providers {",
37
+ ' aws = { source = "hashicorp/aws" }',
38
+ " }",
39
+ "}",
40
+ "",
41
+ 'variable "x" {',
42
+ " type = string",
43
+ "}",
44
+ ].join("\n"),
45
+ );
46
+ expect(scan.blockLineQueues.get("terraform")).toEqual([1]);
47
+ expect(scan.blockLineQueues.get("var.x")).toEqual([7]);
48
+ });
49
+
50
+ test("two blocks sharing one address queue in file order", () => {
51
+ const scan = scanSuppressions(
52
+ "main.tf",
53
+ ['locals {', " a = 1", "}", "", "locals {", " b = 2", "}"].join("\n"),
54
+ );
55
+ expect(scan.blockLineQueues.get("locals")).toEqual([1, 5]);
56
+ });
57
+ });
58
+
59
+ describe("scanSuppressions: directive forms", () => {
60
+ test("chant-ignore anchors to the very next line", () => {
61
+ const scan = scanSuppressions("main.tf", `# chant-ignore: TF010, TF011\nvariable "x" {\n type = string\n}\n`);
62
+ const directives = scan.byAnchorLine.get(2);
63
+ expect(directives).toHaveLength(1);
64
+ expect(directives?.[0].form).toBe("chant-ignore");
65
+ expect(directives?.[0].ids).toEqual(new Set(["TF010", "TF011"]));
66
+ });
67
+
68
+ test("chant-ignore-block parses distinctly from bare chant-ignore", () => {
69
+ const scan = scanSuppressions("main.tf", `# chant-ignore-block: TF001\nterraform {\n}\n`);
70
+ const directives = scan.byAnchorLine.get(2);
71
+ expect(directives?.[0].form).toBe("chant-ignore-block");
72
+ expect(directives?.[0].ids).toEqual(new Set(["TF001"]));
73
+ });
74
+
75
+ test('"all" is the wildcard', () => {
76
+ const scan = scanSuppressions("main.tf", `# chant-ignore: all\nvariable "x" {\n type = string\n}\n`);
77
+ expect(scan.byAnchorLine.get(2)?.[0].ids).toBe("all");
78
+ });
79
+
80
+ test("also works with // comments", () => {
81
+ const scan = scanSuppressions("main.tf", `// chant-ignore: TF010\nvariable "x" {\n type = string\n}\n`);
82
+ expect(scan.byAnchorLine.get(2)?.[0].ids).toEqual(new Set(["TF010"]));
83
+ });
84
+
85
+ test("an exp: term sets expiry and is excluded from the id list", () => {
86
+ const scan = scanSuppressions("main.tf", `# chant-ignore: TF010 exp:2026-12-31\nvariable "x" {\n type = string\n}\n`);
87
+ const d = scan.byAnchorLine.get(2)?.[0];
88
+ expect(d?.ids).toEqual(new Set(["TF010"]));
89
+ expect(d?.expires).toBe("2026-12-31");
90
+ });
91
+
92
+ test("a comment not on the line immediately before a block has no effect on that block", () => {
93
+ // The comment anchors to line 2 (its own line + 1); the block starts at
94
+ // line 3 because of the blank line between them, so `directivesFor` never
95
+ // finds a directive at the block's actual start line.
96
+ const scan = scanSuppressions("main.tf", `# chant-ignore: TF010\n\nvariable "x" {\n type = string\n}\n`);
97
+ const { line, suppressions } = directivesFor(scan, "var.x");
98
+ expect(line).toBe(3);
99
+ expect(suppressions).toBeUndefined();
100
+ });
101
+
102
+ test("chant-ignore-file as the first non-blank line is not misplaced", () => {
103
+ const scan = scanSuppressions("main.tf", `# chant-ignore-file: TF011\nvariable "x" {\n type = string\n}\n`);
104
+ expect(scan.fileDirective?.misplaced).toBeFalsy();
105
+ expect(scan.fileDirective?.ids).toEqual(new Set(["TF011"]));
106
+ });
107
+
108
+ test("chant-ignore-file after another line is misplaced", () => {
109
+ const scan = scanSuppressions("main.tf", `# a license header\n# chant-ignore-file: TF011\nvariable "x" {\n type = string\n}\n`);
110
+ expect(scan.fileDirective?.misplaced).toBe(true);
111
+ });
112
+
113
+ test("chant-ignore-file after leading blank lines is not misplaced (blank lines don't count)", () => {
114
+ const scan = scanSuppressions("main.tf", `\n\n# chant-ignore-file: TF011\nvariable "x" {\n type = string\n}\n`);
115
+ expect(scan.fileDirective?.misplaced).toBeFalsy();
116
+ });
117
+ });
118
+
119
+ describe("directivesFor", () => {
120
+ test("combines an anchored directive with the file-level one, and reports the block's line", () => {
121
+ const source = [
122
+ "# chant-ignore-file: TF011",
123
+ "# chant-ignore: TF010",
124
+ 'variable "x" {',
125
+ " type = string",
126
+ "}",
127
+ ].join("\n");
128
+ const scan = scanSuppressions("main.tf", source);
129
+ const { line, suppressions } = directivesFor(scan, "var.x");
130
+ expect(line).toBe(3);
131
+ expect(suppressions?.map((d) => d.form).sort()).toEqual(["chant-ignore", "chant-ignore-file"]);
132
+ });
133
+
134
+ test("an address with no recorded block line yields no line and no suppressions", () => {
135
+ const scan = scanSuppressions("main.tf", `variable "x" {\n type = string\n}\n`);
136
+ const { line, suppressions } = directivesFor(scan, "var.y");
137
+ expect(line).toBeUndefined();
138
+ expect(suppressions).toBeUndefined();
139
+ });
140
+
141
+ test("popping the same address twice returns each recorded line once (FIFO)", () => {
142
+ const scan = scanSuppressions("main.tf", `locals {\n a = 1\n}\n\nlocals {\n b = 2\n}\n`);
143
+ expect(directivesFor(scan, "locals").line).toBe(1);
144
+ expect(directivesFor(scan, "locals").line).toBe(5);
145
+ expect(directivesFor(scan, "locals").line).toBeUndefined();
146
+ });
147
+ });
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Read `# chant-ignore*` comments out of raw `.tf` text.
3
+ *
4
+ * `hcl2json` drops comments entirely, and a probe of its output confirmed it
5
+ * exposes no source ranges either, so a suppression comment has nothing to
6
+ * attach to once a file goes through `@cdktf/hcl2json`. This module runs a
7
+ * second, cheap pass over the SAME raw text `./parse.ts` hands the parser,
8
+ * entirely with line-oriented regexes: a brace-depth counter to find each
9
+ * top-level block's start line (`terraform`, `locals`, `provider "x"`,
10
+ * `module "x"`, `variable "x"`, `output "x"`, `resource "t" "n"`,
11
+ * `data "t" "n"`, the exact set `blocksToEntities` turns into entities, and
12
+ * the address format below MUST stay in sync with that function's `add()`
13
+ * calls), and a directive regex for the three comment forms.
14
+ *
15
+ * `scanSuppressions` returns per-file lookups keyed by ADDRESS (`./parse.ts`'s
16
+ * `TerraformEntity.props.address`, e.g. `provider.aws`, `aws_s3_bucket.assets`)
17
+ * rather than by entity, because `blocksToEntities` computes an entity's final
18
+ * map key (with a `~2`/`~3` collision suffix) only after this scan has already
19
+ * run. A FIFO queue per address reconciles the two traversal orders: this
20
+ * scan reads the file top to bottom, but `blocksToEntities` walks hcl2json's
21
+ * tree grouped by section (all `variable` blocks together, then all
22
+ * `resource` blocks, etc.), not in source order. Two blocks that share one
23
+ * address (the same genuinely-duplicated declaration) are matched to their
24
+ * queue slot in file order, same as they're numbered `~2`/`~3` in file order.
25
+ */
26
+
27
+ import type { SuppressionDirective, SuppressionIds } from "@intentius/chant/lint/suppressions";
28
+
29
+ /** A block header keyword with zero labels (`terraform {`, `locals {`). */
30
+ const BARE_RE = /^\s*(terraform|locals)\s*\{/;
31
+ /** One label (`provider "aws" {`, `module "vpc" {`, `variable "x" {`, `output "x" {`). */
32
+ const ONE_LABEL_RE = /^\s*(provider|module|variable|output)\s+"([^"]*)"\s*\{/;
33
+ /** Two labels (`resource "aws_s3_bucket" "assets" {`, `data "aws_ami" "x" {`). */
34
+ const TWO_LABEL_RE = /^\s*(resource|data)\s+"([^"]*)"\s+"([^"]*)"\s*\{/;
35
+
36
+ /** A `#`/`//` comment naming one of the three suppression forms. */
37
+ const DIRECTIVE_RE = /^\s*(?:#|\/\/)\s*chant-ignore(-file|-block)?\s*:?\s*(.*)$/;
38
+
39
+ /** `./parse.ts`'s own address format per block kind, kept in sync by hand. */
40
+ function addressFor(keyword: string, labels: string[]): string {
41
+ switch (keyword) {
42
+ case "terraform":
43
+ return "terraform";
44
+ case "locals":
45
+ return "locals";
46
+ case "provider":
47
+ return `provider.${labels[0]}`;
48
+ case "module":
49
+ return `module.${labels[0]}`;
50
+ case "variable":
51
+ return `var.${labels[0]}`;
52
+ case "output":
53
+ return `output.${labels[0]}`;
54
+ case "resource":
55
+ return `${labels[0]}.${labels[1]}`;
56
+ case "data":
57
+ return `data.${labels[0]}.${labels[1]}`;
58
+ default:
59
+ return "";
60
+ }
61
+ }
62
+
63
+ /** Strip quoted string contents so a brace inside a literal (a jsonencode payload) doesn't skew depth tracking. */
64
+ function stripStrings(line: string): string {
65
+ return line.replace(/"(?:[^"\\]|\\.)*"/g, '""');
66
+ }
67
+
68
+ function braceDelta(line: string): number {
69
+ const stripped = stripStrings(line);
70
+ let delta = 0;
71
+ for (const ch of stripped) {
72
+ if (ch === "{") delta++;
73
+ else if (ch === "}") delta--;
74
+ }
75
+ return delta;
76
+ }
77
+
78
+ function parseDirectiveBody(rest: string): { ids: SuppressionIds; expires?: string } {
79
+ const tokens = rest.split(/\s+/).filter(Boolean);
80
+ let expires: string | undefined;
81
+ const idTokens: string[] = [];
82
+ for (const tok of tokens) {
83
+ const m = /^exp:(\d{4}-\d{2}-\d{2})$/.exec(tok);
84
+ if (m) {
85
+ expires = m[1];
86
+ continue;
87
+ }
88
+ idTokens.push(tok);
89
+ }
90
+ const joined = idTokens.join(" ").trim();
91
+ if (joined === "" || joined.toLowerCase() === "all") return { ids: "all", expires };
92
+ const ids = new Set(
93
+ joined
94
+ .split(/[,\s]+/)
95
+ .map((s) => s.trim().toUpperCase())
96
+ .filter(Boolean),
97
+ );
98
+ return { ids, expires };
99
+ }
100
+
101
+ /** One file's suppression scan: block start lines by address, and the parsed directives. */
102
+ export interface FileScan {
103
+ /** address -> start lines, in file order (a FIFO queue for repeated addresses). */
104
+ blockLineQueues: Map<string, number[]>;
105
+ /** The block start line a `chant-ignore`/`chant-ignore-block` comment anchors to -> directives found there. */
106
+ byAnchorLine: Map<number, SuppressionDirective[]>;
107
+ /** The file's `chant-ignore-file` directive, if any (`misplaced` set when it wasn't the first non-blank line). */
108
+ fileDirective?: SuppressionDirective;
109
+ }
110
+
111
+ function recordBlockLine(queues: Map<string, number[]>, address: string, lineNo: number): void {
112
+ const q = queues.get(address) ?? [];
113
+ q.push(lineNo);
114
+ queues.set(address, q);
115
+ }
116
+
117
+ /**
118
+ * Scan one `.tf` file's raw text. `fileName` only labels the directives it
119
+ * returns (for messages and dedup); the scan itself is pure text.
120
+ */
121
+ export function scanSuppressions(fileName: string, source: string): FileScan {
122
+ const lines = source.split("\n");
123
+ const blockLineQueues = new Map<string, number[]>();
124
+ const byAnchorLine = new Map<number, SuppressionDirective[]>();
125
+ let fileDirective: SuppressionDirective | undefined;
126
+ let firstNonBlankLine: number | undefined;
127
+ let depth = 0;
128
+
129
+ for (let i = 0; i < lines.length; i++) {
130
+ const line = lines[i];
131
+ const lineNo = i + 1;
132
+ const trimmed = line.trim();
133
+
134
+ if (firstNonBlankLine === undefined && trimmed !== "") firstNonBlankLine = lineNo;
135
+
136
+ const directiveMatch = DIRECTIVE_RE.exec(line);
137
+ if (directiveMatch) {
138
+ const suffix = directiveMatch[1];
139
+ const { ids, expires } = parseDirectiveBody(directiveMatch[2].trim());
140
+
141
+ if (suffix === "-file") {
142
+ fileDirective = {
143
+ form: "chant-ignore-file",
144
+ ids,
145
+ expires,
146
+ file: fileName,
147
+ line: lineNo,
148
+ misplaced: lineNo !== firstNonBlankLine,
149
+ key: `${fileName}:${lineNo}:chant-ignore-file`,
150
+ };
151
+ } else {
152
+ const form = suffix === "-block" ? "chant-ignore-block" : "chant-ignore";
153
+ const directive: SuppressionDirective = {
154
+ form,
155
+ ids,
156
+ expires,
157
+ file: fileName,
158
+ line: lineNo,
159
+ key: `${fileName}:${lineNo}:${form}`,
160
+ };
161
+ const anchorLine = lineNo + 1;
162
+ const list = byAnchorLine.get(anchorLine) ?? [];
163
+ list.push(directive);
164
+ byAnchorLine.set(anchorLine, list);
165
+ }
166
+ } else if (depth === 0) {
167
+ // Only look for a block header on a line that isn't itself a directive
168
+ // comment, and only at top-level nesting (depth 0 before this line).
169
+ let m: RegExpExecArray | null = BARE_RE.exec(line);
170
+ if (m) {
171
+ recordBlockLine(blockLineQueues, addressFor(m[1], []), lineNo);
172
+ } else if ((m = ONE_LABEL_RE.exec(line))) {
173
+ recordBlockLine(blockLineQueues, addressFor(m[1], [m[2]]), lineNo);
174
+ } else if ((m = TWO_LABEL_RE.exec(line))) {
175
+ recordBlockLine(blockLineQueues, addressFor(m[1], [m[2], m[3]]), lineNo);
176
+ }
177
+ }
178
+
179
+ depth += braceDelta(line);
180
+ }
181
+
182
+ return { blockLineQueues, byAnchorLine, fileDirective };
183
+ }
184
+
185
+ /** Pop the next recorded start line for `address` (FIFO; see module doc). */
186
+ export function popBlockLine(scan: FileScan, address: string): number | undefined {
187
+ const q = scan.blockLineQueues.get(address);
188
+ if (!q || q.length === 0) return undefined;
189
+ return q.shift();
190
+ }
191
+
192
+ /**
193
+ * Directives to attach to the entity being added for `address`: the ones
194
+ * anchored to its own start line (`chant-ignore`/`chant-ignore-block`), plus
195
+ * the file-level one, if any. Every entity in a file carries a file-level
196
+ * directive, misplaced or not (misplaced ones report themselves but suppress
197
+ * nothing; see `applyInlineSuppressions`).
198
+ */
199
+ export function directivesFor(scan: FileScan, address: string): { line?: number; suppressions?: SuppressionDirective[] } {
200
+ const line = popBlockLine(scan, address);
201
+ const anchored = line !== undefined ? (scan.byAnchorLine.get(line) ?? []) : [];
202
+ const all = scan.fileDirective ? [...anchored, scan.fileDirective] : anchored;
203
+ return { line, suppressions: all.length > 0 ? all : undefined };
204
+ }