@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.
- package/dist/composites/terraform-adopt-op.d.ts +126 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +63 -16
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +80 -36
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/config.d.ts +49 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +256 -16
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/descend.d.ts +98 -0
- package/dist/hcl/descend.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts +130 -0
- package/dist/hcl/marker.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +142 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/references.d.ts +76 -0
- package/dist/hcl/references.d.ts.map +1 -0
- package/dist/hcl/roots.d.ts +22 -3
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts +54 -0
- package/dist/hcl/suppressions.d.ts.map +1 -0
- package/dist/hcl/value.d.ts +77 -0
- package/dist/hcl/value.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +32 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +68 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/blocks.d.ts +53 -0
- package/dist/lint/post-synth/blocks.d.ts.map +1 -0
- package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
- package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +38 -0
- package/dist/lint/post-synth/module-source.d.ts.map +1 -0
- package/dist/lint/post-synth/scope.d.ts +65 -0
- package/dist/lint/post-synth/scope.d.ts.map +1 -0
- package/dist/lint/post-synth/tf001.d.ts +18 -0
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf002.d.ts +32 -0
- package/dist/lint/post-synth/tf002.d.ts.map +1 -0
- package/dist/lint/post-synth/tf003.d.ts +22 -0
- package/dist/lint/post-synth/tf003.d.ts.map +1 -0
- package/dist/lint/post-synth/tf004.d.ts +22 -0
- package/dist/lint/post-synth/tf004.d.ts.map +1 -0
- package/dist/lint/post-synth/tf005.d.ts +28 -0
- package/dist/lint/post-synth/tf005.d.ts.map +1 -0
- package/dist/lint/post-synth/tf006.d.ts +21 -0
- package/dist/lint/post-synth/tf006.d.ts.map +1 -0
- package/dist/lint/post-synth/tf007.d.ts +21 -0
- package/dist/lint/post-synth/tf007.d.ts.map +1 -0
- package/dist/lint/post-synth/tf008.d.ts +22 -0
- package/dist/lint/post-synth/tf008.d.ts.map +1 -0
- package/dist/lint/post-synth/tf009.d.ts +24 -0
- package/dist/lint/post-synth/tf009.d.ts.map +1 -0
- package/dist/lint/post-synth/tf010.d.ts +19 -0
- package/dist/lint/post-synth/tf010.d.ts.map +1 -0
- package/dist/lint/post-synth/tf011.d.ts +18 -0
- package/dist/lint/post-synth/tf011.d.ts.map +1 -0
- package/dist/lint/post-synth/tf012.d.ts +17 -0
- package/dist/lint/post-synth/tf012.d.ts.map +1 -0
- package/dist/lint/post-synth/tf013.d.ts +21 -0
- package/dist/lint/post-synth/tf013.d.ts.map +1 -0
- package/dist/lint/post-synth/tf014.d.ts +25 -0
- package/dist/lint/post-synth/tf014.d.ts.map +1 -0
- package/dist/lint/post-synth/tf015.d.ts +21 -0
- package/dist/lint/post-synth/tf015.d.ts.map +1 -0
- package/dist/lint/post-synth/tf016.d.ts +30 -0
- package/dist/lint/post-synth/tf016.d.ts.map +1 -0
- package/dist/lint/post-synth/tf017.d.ts +19 -0
- package/dist/lint/post-synth/tf017.d.ts.map +1 -0
- package/dist/lint/post-synth/tf018.d.ts +20 -0
- package/dist/lint/post-synth/tf018.d.ts.map +1 -0
- package/dist/lint/post-synth/tf019.d.ts +25 -0
- package/dist/lint/post-synth/tf019.d.ts.map +1 -0
- package/dist/lint/post-synth/tf020.d.ts +38 -0
- package/dist/lint/post-synth/tf020.d.ts.map +1 -0
- package/dist/lint/post-synth/tf021.d.ts +41 -0
- package/dist/lint/post-synth/tf021.d.ts.map +1 -0
- package/dist/lint/post-synth/tf022.d.ts +24 -0
- package/dist/lint/post-synth/tf022.d.ts.map +1 -0
- package/dist/lint/post-synth/tf024.d.ts +24 -0
- package/dist/lint/post-synth/tf024.d.ts.map +1 -0
- package/dist/lint/post-synth/tf025.d.ts +29 -0
- package/dist/lint/post-synth/tf025.d.ts.map +1 -0
- package/dist/lint/post-synth/tf026.d.ts +21 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -0
- package/dist/lint/rules/fixtures/load.d.ts +13 -0
- package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
- package/dist/lint/rules/plan-before-apply.d.ts +11 -0
- package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
- package/dist/lint/secret-shape.d.ts +68 -0
- package/dist/lint/secret-shape.d.ts.map +1 -0
- package/dist/live-ownership.d.ts +55 -0
- package/dist/live-ownership.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +24 -0
- package/dist/okf/rules/TF002.md +11 -0
- package/dist/okf/rules/TF003.md +11 -0
- package/dist/okf/rules/TF004.md +11 -0
- package/dist/okf/rules/TF005.md +11 -0
- package/dist/okf/rules/TF006.md +11 -0
- package/dist/okf/rules/TF007.md +11 -0
- package/dist/okf/rules/TF008.md +11 -0
- package/dist/okf/rules/TF009.md +11 -0
- package/dist/okf/rules/TF010.md +11 -0
- package/dist/okf/rules/TF011.md +11 -0
- package/dist/okf/rules/TF012.md +11 -0
- package/dist/okf/rules/TF013.md +11 -0
- package/dist/okf/rules/TF014.md +11 -0
- package/dist/okf/rules/TF015.md +11 -0
- package/dist/okf/rules/TF016.md +11 -0
- package/dist/okf/rules/TF017.md +11 -0
- package/dist/okf/rules/TF018.md +11 -0
- package/dist/okf/rules/TF019.md +11 -0
- package/dist/okf/rules/TF020.md +11 -0
- package/dist/okf/rules/TF021.md +11 -0
- package/dist/okf/rules/TF022.md +11 -0
- package/dist/okf/rules/TF024.md +11 -0
- package/dist/okf/rules/TF025.md +11 -0
- package/dist/okf/rules/TF026.md +11 -0
- package/dist/op/activities/index.d.ts +13 -6
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/live-detect.d.ts +51 -0
- package/dist/op/activities/live-detect.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +445 -17
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +148 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/adoption.d.ts +118 -0
- package/dist/op/adoption.d.ts.map +1 -0
- package/dist/op/builders.d.ts +46 -6
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +45 -0
- package/dist/op/resolve-root-mode.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/blocks.ts +109 -0
- package/dist/rules/module-source.ts +121 -0
- package/dist/rules/plan-before-apply.ts +11 -0
- package/dist/rules/scope.ts +107 -0
- package/dist/rules/tf001.ts +32 -8
- package/dist/rules/tf002.ts +151 -0
- package/dist/rules/tf003.ts +71 -0
- package/dist/rules/tf004.ts +63 -0
- package/dist/rules/tf005.ts +76 -0
- package/dist/rules/tf006.ts +53 -0
- package/dist/rules/tf007.ts +78 -0
- package/dist/rules/tf008.ts +75 -0
- package/dist/rules/tf009.ts +59 -0
- package/dist/rules/tf010.ts +51 -0
- package/dist/rules/tf011.ts +49 -0
- package/dist/rules/tf012.ts +47 -0
- package/dist/rules/tf013.ts +63 -0
- package/dist/rules/tf014.ts +72 -0
- package/dist/rules/tf015.ts +63 -0
- package/dist/rules/tf016.ts +75 -0
- package/dist/rules/tf017.ts +50 -0
- package/dist/rules/tf018.ts +72 -0
- package/dist/rules/tf019.ts +72 -0
- package/dist/rules/tf020.ts +123 -0
- package/dist/rules/tf021.ts +126 -0
- package/dist/rules/tf022.ts +61 -0
- package/dist/rules/tf024.ts +70 -0
- package/dist/rules/tf025.ts +101 -0
- package/dist/rules/tf026.ts +80 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/package.json +7 -2
- package/src/__fixtures__/live/main.tf +28 -0
- package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
- package/src/__fixtures__/live-adopt/main.tf +21 -0
- package/src/__fixtures__/live-estate/README.md +58 -0
- package/src/__fixtures__/live-estate/compute.tf +10 -0
- package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-estate/logs.tf +8 -0
- package/src/__fixtures__/live-estate/network.tf +24 -0
- package/src/__fixtures__/live-estate/rule.tf +11 -0
- package/src/__fixtures__/live-estate/unowned.tf +21 -0
- package/src/__fixtures__/live-estate/versions.tf +31 -0
- package/src/__fixtures__/live-ls.json +98 -0
- package/src/__fixtures__/live-plan.json +285 -0
- package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
- package/src/__fixtures__/live-sidecar/main.tf +24 -0
- package/src/__fixtures__/module-tree/README.md +16 -0
- package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
- package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
- package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
- package/src/__fixtures__/module-tree/missing/main.tf +9 -0
- package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
- package/src/__fixtures__/module-tree/root/main.tf +28 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
- package/src/__fixtures__/module-tree/shared/main.tf +5 -0
- package/src/__fixtures__/references/main.tf +56 -0
- package/src/codegen/lint-rules-page.test.ts +28 -0
- package/src/composites/terraform-adopt-op.acceptance.test.ts +166 -0
- package/src/composites/terraform-adopt-op.test.ts +170 -0
- package/src/composites/terraform-adopt-op.ts +197 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +183 -5
- package/src/composites/terraform-apply-op.test.ts +167 -15
- package/src/composites/terraform-apply-op.ts +105 -28
- package/src/composites/terraform-watch-op.test.ts +85 -26
- package/src/composites/terraform-watch-op.ts +130 -73
- package/src/config.ts +32 -3
- package/src/describe-resources.live.test.ts +598 -0
- package/src/describe-resources.test.ts +53 -0
- package/src/describe-resources.ts +811 -28
- package/src/hcl/descend.test.ts +162 -0
- package/src/hcl/descend.ts +273 -0
- package/src/hcl/marker.test.ts +147 -0
- package/src/hcl/marker.ts +235 -0
- package/src/hcl/parse.test.ts +69 -0
- package/src/hcl/parse.ts +249 -27
- package/src/hcl/references.test.ts +116 -0
- package/src/hcl/references.ts +172 -0
- package/src/hcl/roots.ts +59 -5
- package/src/hcl/suppressions.test.ts +147 -0
- package/src/hcl/suppressions.ts +204 -0
- package/src/hcl/value.test.ts +73 -0
- package/src/hcl/value.ts +106 -0
- package/src/index.ts +31 -1
- package/src/lint/audit-catalog.ts +264 -3
- package/src/lint/audit-lineage.ts +319 -1
- package/src/lint/audit.test.ts +40 -1
- package/src/lint/post-synth/blocks.ts +109 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
- package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
- package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
- package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
- package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
- package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
- package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
- package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
- package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
- package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
- package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
- package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
- package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
- package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
- package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
- package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
- package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
- package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
- package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
- package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
- package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
- package/src/lint/post-synth/fixtures/load.ts +108 -0
- package/src/lint/post-synth/index.ts +48 -0
- package/src/lint/post-synth/module-rules.test.ts +153 -0
- package/src/lint/post-synth/module-source.test.ts +75 -0
- package/src/lint/post-synth/module-source.ts +121 -0
- package/src/lint/post-synth/post-synth.test.ts +14 -15
- package/src/lint/post-synth/scope.ts +107 -0
- package/src/lint/post-synth/single-block.test.ts +263 -0
- package/src/lint/post-synth/suppression.test.ts +110 -0
- package/src/lint/post-synth/tf001.ts +32 -8
- package/src/lint/post-synth/tf002.test.ts +93 -0
- package/src/lint/post-synth/tf002.ts +151 -0
- package/src/lint/post-synth/tf003.test.ts +75 -0
- package/src/lint/post-synth/tf003.ts +71 -0
- package/src/lint/post-synth/tf004.test.ts +47 -0
- package/src/lint/post-synth/tf004.ts +63 -0
- package/src/lint/post-synth/tf005.test.ts +57 -0
- package/src/lint/post-synth/tf005.ts +76 -0
- package/src/lint/post-synth/tf006.ts +53 -0
- package/src/lint/post-synth/tf007.ts +78 -0
- package/src/lint/post-synth/tf008.ts +75 -0
- package/src/lint/post-synth/tf009.ts +59 -0
- package/src/lint/post-synth/tf010.ts +51 -0
- package/src/lint/post-synth/tf011.ts +49 -0
- package/src/lint/post-synth/tf012.ts +47 -0
- package/src/lint/post-synth/tf013.ts +63 -0
- package/src/lint/post-synth/tf014.ts +72 -0
- package/src/lint/post-synth/tf015.ts +63 -0
- package/src/lint/post-synth/tf016.ts +75 -0
- package/src/lint/post-synth/tf017.ts +50 -0
- package/src/lint/post-synth/tf018.ts +72 -0
- package/src/lint/post-synth/tf019.ts +72 -0
- package/src/lint/post-synth/tf020.ts +123 -0
- package/src/lint/post-synth/tf021.ts +126 -0
- package/src/lint/post-synth/tf022.ts +61 -0
- package/src/lint/post-synth/tf024.test.ts +42 -0
- package/src/lint/post-synth/tf024.ts +70 -0
- package/src/lint/post-synth/tf025.test.ts +46 -0
- package/src/lint/post-synth/tf025.ts +101 -0
- package/src/lint/post-synth/tf026.test.ts +56 -0
- package/src/lint/post-synth/tf026.ts +80 -0
- package/src/lint/presets.test.ts +72 -0
- package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
- package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
- package/src/lint/rules/fixtures/load.ts +53 -0
- package/src/lint/rules/plan-before-apply.test.ts +112 -7
- package/src/lint/rules/plan-before-apply.ts +11 -0
- package/src/lint/secret-shape.test.ts +150 -0
- package/src/lint/secret-shape.ts +159 -0
- package/src/live-ownership.ts +60 -0
- package/src/lsp/completions.test.ts +3 -2
- package/src/lsp/option-keys.ts +10 -5
- package/src/op/activities/choudoufu.acceptance.test.ts +143 -0
- package/src/op/activities/choudoufu.test.ts +693 -0
- package/src/op/activities/index.ts +40 -4
- package/src/op/activities/live-detect.test.ts +143 -0
- package/src/op/activities/live-detect.ts +162 -0
- package/src/op/activities/registry.test.ts +11 -3
- package/src/op/activities/terraform.test.ts +7 -3
- package/src/op/activities/terraform.ts +823 -69
- package/src/op/activity-contracts.test.ts +147 -0
- package/src/op/activity-contracts.ts +178 -0
- package/src/op/adoption.test.ts +170 -0
- package/src/op/adoption.ts +241 -0
- package/src/op/builders.test.ts +60 -5
- package/src/op/builders.ts +93 -5
- package/src/op/resolve-root-mode.test.ts +85 -0
- package/src/op/resolve-root-mode.ts +72 -0
- package/src/plugin.test.ts +68 -0
- package/src/plugin.ts +75 -9
- package/src/skills/chant-terraform.md +1 -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
|
|
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
|
|
41
|
-
*
|
|
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
|
|
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
|
+
}
|