@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Core lint rules for chant projects (COR + EVL).
3
+ */
4
+
5
+ import type { LintRule } from "../rule";
6
+
7
+ export { flatDeclarationsRule } from "./flat-declarations";
8
+ export { exportRequiredRule } from "./export-required";
9
+ export { fileDeclarableLimitRule } from "./file-declarable-limit";
10
+ export { singleConcernFileRule } from "./single-concern-file";
11
+ export { preferNamespaceImportRule } from "./prefer-namespace-import";
12
+ export { barrelImportStyleRule } from "./barrel-import-style";
13
+ export { declarableNamingConventionRule } from "./declarable-naming-convention";
14
+ export { noUnusedDeclarableImportRule } from "./no-unused-declarable-import";
15
+ export { noRedundantValueCastRule } from "./no-redundant-value-cast";
16
+ export { noUnusedDeclarableRule } from "./no-unused-declarable";
17
+ export { noCyclicDeclarableRefRule } from "./no-cyclic-declarable-ref";
18
+ export { noRedundantTypeImportRule } from "./no-redundant-type-import";
19
+ export { noStringRefRule } from "./no-string-ref";
20
+ export { enforceBarrelImportRule } from "./enforce-barrel-import";
21
+ export { enforceBarrelRefRule } from "./enforce-barrel-ref";
22
+ export { evl001NonLiteralExpressionRule } from "./evl001-non-literal-expression";
23
+ export { evl002ControlFlowResourceRule } from "./evl002-control-flow-resource";
24
+ export { evl003DynamicPropertyAccessRule } from "./evl003-dynamic-property-access";
25
+ export { evl004SpreadNonConstRule } from "./evl004-spread-non-const";
26
+ export { evl005ResourceBlockBodyRule } from "./evl005-resource-block-body";
27
+ export { evl006BarrelUsageRule } from "./evl006-barrel-usage";
28
+ export { evl007InvalidSiblingsRule } from "./evl007-invalid-siblings";
29
+ export { evl008UnresolvableBarrelRefRule } from "./evl008-unresolvable-barrel-ref";
30
+ export { staleBarrelTypesRule } from "./stale-barrel-types";
31
+ export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
32
+ export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
33
+ export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
34
+ export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
35
+
36
+ import { flatDeclarationsRule } from "./flat-declarations";
37
+ import { exportRequiredRule } from "./export-required";
38
+ import { fileDeclarableLimitRule } from "./file-declarable-limit";
39
+ import { singleConcernFileRule } from "./single-concern-file";
40
+ import { preferNamespaceImportRule } from "./prefer-namespace-import";
41
+ import { barrelImportStyleRule } from "./barrel-import-style";
42
+ import { declarableNamingConventionRule } from "./declarable-naming-convention";
43
+ import { noUnusedDeclarableImportRule } from "./no-unused-declarable-import";
44
+ import { noRedundantValueCastRule } from "./no-redundant-value-cast";
45
+ import { noUnusedDeclarableRule } from "./no-unused-declarable";
46
+ import { noCyclicDeclarableRefRule } from "./no-cyclic-declarable-ref";
47
+ import { noRedundantTypeImportRule } from "./no-redundant-type-import";
48
+ import { noStringRefRule } from "./no-string-ref";
49
+ import { enforceBarrelImportRule } from "./enforce-barrel-import";
50
+ import { enforceBarrelRefRule } from "./enforce-barrel-ref";
51
+ import { evl001NonLiteralExpressionRule } from "./evl001-non-literal-expression";
52
+ import { evl002ControlFlowResourceRule } from "./evl002-control-flow-resource";
53
+ import { evl003DynamicPropertyAccessRule } from "./evl003-dynamic-property-access";
54
+ import { evl004SpreadNonConstRule } from "./evl004-spread-non-const";
55
+ import { evl005ResourceBlockBodyRule } from "./evl005-resource-block-body";
56
+ import { evl006BarrelUsageRule } from "./evl006-barrel-usage";
57
+ import { evl007InvalidSiblingsRule } from "./evl007-invalid-siblings";
58
+ import { evl008UnresolvableBarrelRefRule } from "./evl008-unresolvable-barrel-ref";
59
+ import { staleBarrelTypesRule } from "./stale-barrel-types";
60
+ import { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
61
+ import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
62
+ import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
63
+ import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
64
+
65
+ /**
66
+ * Load all 28 core lint rules (COR + EVL).
67
+ */
68
+ export function loadCoreRules(): LintRule[] {
69
+ return [
70
+ flatDeclarationsRule,
71
+ exportRequiredRule,
72
+ fileDeclarableLimitRule,
73
+ singleConcernFileRule,
74
+ preferNamespaceImportRule,
75
+ barrelImportStyleRule,
76
+ declarableNamingConventionRule,
77
+ noUnusedDeclarableImportRule,
78
+ noRedundantValueCastRule,
79
+ noUnusedDeclarableRule,
80
+ noCyclicDeclarableRefRule,
81
+ noRedundantTypeImportRule,
82
+ noStringRefRule,
83
+ enforceBarrelImportRule,
84
+ enforceBarrelRefRule,
85
+ evl001NonLiteralExpressionRule,
86
+ evl002ControlFlowResourceRule,
87
+ evl003DynamicPropertyAccessRule,
88
+ evl004SpreadNonConstRule,
89
+ evl005ResourceBlockBodyRule,
90
+ evl006BarrelUsageRule,
91
+ evl007InvalidSiblingsRule,
92
+ evl008UnresolvableBarrelRefRule,
93
+ staleBarrelTypesRule,
94
+ evl009CompositeNoConstantRule,
95
+ evl010CompositeNoTransformRule,
96
+ cor017CompositeNameMatchRule,
97
+ cor018CompositePreferLexiconTypeRule,
98
+ ];
99
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noCyclicDeclarableRefRule } from "./no-cyclic-declarable-ref";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR011: no-cyclic-declarable-ref", () => {
12
+ test("rule metadata", () => {
13
+ expect(noCyclicDeclarableRefRule.id).toBe("COR011");
14
+ expect(noCyclicDeclarableRefRule.severity).toBe("error");
15
+ expect(noCyclicDeclarableRefRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("flags direct cycle: A references B, B references A", () => {
19
+ const ctx = createContext(
20
+ `export const bucket = new Bucket({ role: role.arn });\n` +
21
+ `export const role = new Role({ bucket: bucket.bucketName });`,
22
+ );
23
+ const diags = noCyclicDeclarableRefRule.check(ctx);
24
+ expect(diags).toHaveLength(1);
25
+ expect(diags[0].ruleId).toBe("COR011");
26
+ expect(diags[0].severity).toBe("error");
27
+ expect(diags[0].message).toContain("bucket");
28
+ expect(diags[0].message).toContain("role");
29
+ expect(diags[0].message).toContain("Circular reference detected");
30
+ });
31
+
32
+ test("flags transitive cycle: A→B→C→A", () => {
33
+ const ctx = createContext(
34
+ `export const a = new ResourceA({ ref: b.id });\n` +
35
+ `export const b = new ResourceB({ ref: c.id });\n` +
36
+ `export const c = new ResourceC({ ref: a.id });`,
37
+ );
38
+ const diags = noCyclicDeclarableRefRule.check(ctx);
39
+ expect(diags).toHaveLength(1);
40
+ expect(diags[0].message).toContain("a");
41
+ expect(diags[0].message).toContain("b");
42
+ expect(diags[0].message).toContain("c");
43
+ });
44
+
45
+ test("OK: linear chain A→B→C (no cycle)", () => {
46
+ const ctx = createContext(
47
+ `export const a = new ResourceA({});\n` +
48
+ `export const b = new ResourceB({ ref: a.id });\n` +
49
+ `export const c = new ResourceC({ ref: b.id });`,
50
+ );
51
+ const diags = noCyclicDeclarableRefRule.check(ctx);
52
+ expect(diags).toHaveLength(0);
53
+ });
54
+
55
+ test("OK: no cross-references between declarables", () => {
56
+ const ctx = createContext(
57
+ `export const bucket = new Bucket({ bucketName: "x" });\n` +
58
+ `export const role = new Role({ roleName: "y" });\n` +
59
+ `export const fn = new Function({ name: "z" });`,
60
+ );
61
+ const diags = noCyclicDeclarableRefRule.check(ctx);
62
+ expect(diags).toHaveLength(0);
63
+ });
64
+
65
+ test("OK: single declarable (cannot form a cycle)", () => {
66
+ const ctx = createContext(`export const bucket = new Bucket({ bucketName: "x" });`);
67
+ const diags = noCyclicDeclarableRefRule.check(ctx);
68
+ expect(diags).toHaveLength(0);
69
+ });
70
+
71
+ test("flags cycle with namespace import style", () => {
72
+ const ctx = createContext(
73
+ `import * as td from "@intentius/chant-lexicon-testdom";\n` +
74
+ `export const bucket = new td.Bucket({ role: role.arn });\n` +
75
+ `export const role = new td.Role({ bucket: bucket.bucketName });`,
76
+ );
77
+ const diags = noCyclicDeclarableRefRule.check(ctx);
78
+ expect(diags).toHaveLength(1);
79
+ expect(diags[0].message).toContain("Circular reference detected");
80
+ });
81
+
82
+ test("reports correct file path", () => {
83
+ const ctx = createContext(
84
+ `export const bucket = new Bucket({ role: role.arn });\n` +
85
+ `export const role = new Role({ bucket: bucket.bucketName });`,
86
+ "infra/storage.ts",
87
+ );
88
+ const diags = noCyclicDeclarableRefRule.check(ctx);
89
+ expect(diags).toHaveLength(1);
90
+ expect(diags[0].file).toBe("infra/storage.ts");
91
+ });
92
+
93
+ test("reports correct line and column", () => {
94
+ const ctx = createContext(
95
+ `import * as td from "@intentius/chant-lexicon-testdom";\n` +
96
+ `export const bucket = new td.Bucket({ role: role.arn });\n` +
97
+ `export const role = new td.Role({ bucket: bucket.bucketName });`,
98
+ );
99
+ const diags = noCyclicDeclarableRefRule.check(ctx);
100
+ expect(diags).toHaveLength(1);
101
+ expect(diags[0].line).toBe(2);
102
+ expect(diags[0].column).toBe(1);
103
+ });
104
+
105
+ test("message includes arrow-separated cycle path", () => {
106
+ const ctx = createContext(
107
+ `export const bucket = new Bucket({ role: role.arn });\n` +
108
+ `export const role = new Role({ bucket: bucket.bucketName });`,
109
+ );
110
+ const diags = noCyclicDeclarableRefRule.check(ctx);
111
+ expect(diags).toHaveLength(1);
112
+ // Should contain the cycle path with arrows
113
+ expect(diags[0].message).toMatch(/\u2192/);
114
+ expect(diags[0].message).toContain("Break the cycle by restructuring your declarations");
115
+ });
116
+
117
+ test("OK: non-exported declarables are ignored", () => {
118
+ const ctx = createContext(
119
+ `const bucket = new Bucket({ role: role.arn });\n` +
120
+ `const role = new Role({ bucket: bucket.bucketName });`,
121
+ );
122
+ const diags = noCyclicDeclarableRefRule.check(ctx);
123
+ expect(diags).toHaveLength(0);
124
+ });
125
+
126
+ test("detects cycle via direct identifier reference (not property access)", () => {
127
+ const ctx = createContext(
128
+ `export const bucket = new Bucket({ dep: role });\n` +
129
+ `export const role = new Role({ dep: bucket });`,
130
+ );
131
+ const diags = noCyclicDeclarableRefRule.check(ctx);
132
+ expect(diags).toHaveLength(1);
133
+ expect(diags[0].message).toContain("Circular reference detected");
134
+ });
135
+ });
@@ -0,0 +1,178 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR011: no-cyclic-declarable-ref
6
+ *
7
+ * Detects circular references between declarables in the same file.
8
+ * Builds a directed graph of declarable references and reports any cycles found.
9
+ *
10
+ * Triggers on: A references B, B references A (or longer transitive cycles)
11
+ * OK: A→B→C (linear chain, no cycle)
12
+ */
13
+
14
+ interface DeclarableInfo {
15
+ name: string;
16
+ node: ts.VariableStatement;
17
+ initializer: ts.NewExpression;
18
+ }
19
+
20
+ function isCapitalized(name: string): boolean {
21
+ return name.length > 0 && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase();
22
+ }
23
+
24
+ function getNewExpressionClassName(expr: ts.NewExpression): string | undefined {
25
+ if (ts.isIdentifier(expr.expression)) {
26
+ return expr.expression.text;
27
+ }
28
+ if (ts.isPropertyAccessExpression(expr.expression)) {
29
+ return expr.expression.name.text;
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ function collectExportedDeclarables(sourceFile: ts.SourceFile): DeclarableInfo[] {
35
+ const declarables: DeclarableInfo[] = [];
36
+
37
+ ts.forEachChild(sourceFile, (node) => {
38
+ if (!ts.isVariableStatement(node)) return;
39
+
40
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
41
+ if (!hasExport) return;
42
+
43
+ for (const decl of node.declarationList.declarations) {
44
+ if (!ts.isIdentifier(decl.name)) continue;
45
+ if (!decl.initializer) continue;
46
+ if (!ts.isNewExpression(decl.initializer)) continue;
47
+
48
+ const className = getNewExpressionClassName(decl.initializer);
49
+ if (!className || !isCapitalized(className)) continue;
50
+
51
+ declarables.push({
52
+ name: decl.name.text,
53
+ node,
54
+ initializer: decl.initializer,
55
+ });
56
+ }
57
+ });
58
+
59
+ return declarables;
60
+ }
61
+
62
+ function collectReferencedDeclarables(
63
+ initializerNode: ts.Node,
64
+ declarableNames: Set<string>,
65
+ ): Set<string> {
66
+ const refs = new Set<string>();
67
+
68
+ function visit(node: ts.Node): void {
69
+ // Match property access like `role.arn` or plain identifier references
70
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
71
+ if (declarableNames.has(node.expression.text)) {
72
+ refs.add(node.expression.text);
73
+ }
74
+ } else if (ts.isIdentifier(node)) {
75
+ // Direct identifier reference (e.g., passing `bucket` as an argument)
76
+ if (declarableNames.has(node.text)) {
77
+ // Make sure it's not part of a property access we already handled
78
+ const parent = node.parent;
79
+ if (!parent || !ts.isPropertyAccessExpression(parent) || parent.expression !== node) {
80
+ refs.add(node.text);
81
+ }
82
+ }
83
+ }
84
+
85
+ ts.forEachChild(node, visit);
86
+ }
87
+
88
+ visit(initializerNode);
89
+ return refs;
90
+ }
91
+
92
+ function detectCycles(graph: Record<string, string[]>): string[][] {
93
+ const cycles: string[][] = [];
94
+ const visited = new Set<string>();
95
+ const recStack = new Set<string>();
96
+ const path: string[] = [];
97
+
98
+ function dfs(node: string): void {
99
+ visited.add(node);
100
+ recStack.add(node);
101
+ path.push(node);
102
+
103
+ const neighbors = graph[node] || [];
104
+ for (const neighbor of neighbors) {
105
+ if (!visited.has(neighbor)) {
106
+ dfs(neighbor);
107
+ } else if (recStack.has(neighbor)) {
108
+ const cycleStartIndex = path.indexOf(neighbor);
109
+ const cycle = path.slice(cycleStartIndex);
110
+ cycles.push(cycle);
111
+ }
112
+ }
113
+
114
+ recStack.delete(node);
115
+ path.pop();
116
+ }
117
+
118
+ for (const node of Object.keys(graph)) {
119
+ if (!visited.has(node)) {
120
+ dfs(node);
121
+ }
122
+ }
123
+
124
+ return cycles;
125
+ }
126
+
127
+ export const noCyclicDeclarableRefRule: LintRule = {
128
+ id: "COR011",
129
+ severity: "error",
130
+ category: "correctness",
131
+ check(context: LintContext): LintDiagnostic[] {
132
+ const diagnostics: LintDiagnostic[] = [];
133
+ const declarables = collectExportedDeclarables(context.sourceFile);
134
+
135
+ if (declarables.length < 2) return diagnostics;
136
+
137
+ const declarableNames = new Set(declarables.map((d) => d.name));
138
+ const declarableMap = new Map(declarables.map((d) => [d.name, d]));
139
+
140
+ // Build directed graph: A → B means A's constructor references B
141
+ const graph: Record<string, string[]> = {};
142
+ for (const decl of declarables) {
143
+ const refs = collectReferencedDeclarables(decl.initializer, declarableNames);
144
+ // Exclude self-references
145
+ refs.delete(decl.name);
146
+ graph[decl.name] = [...refs];
147
+ }
148
+
149
+ const cycles = detectCycles(graph);
150
+
151
+ // Track which nodes we've already reported to avoid duplicate diagnostics
152
+ const reported = new Set<string>();
153
+
154
+ for (const cycle of cycles) {
155
+ // Create a canonical key for the cycle to deduplicate
156
+ const cycleKey = [...cycle].sort().join(",");
157
+ if (reported.has(cycleKey)) continue;
158
+ reported.add(cycleKey);
159
+
160
+ const cyclePath = [...cycle, cycle[0]].join(" \u2192 ");
161
+ const firstDecl = declarableMap.get(cycle[0])!;
162
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
163
+ firstDecl.node.getStart(context.sourceFile),
164
+ );
165
+
166
+ diagnostics.push({
167
+ file: context.filePath,
168
+ line: line + 1,
169
+ column: character + 1,
170
+ ruleId: "COR011",
171
+ severity: "error",
172
+ message: `Circular reference detected: ${cyclePath}. Break the cycle by restructuring your declarations.`,
173
+ });
174
+ }
175
+
176
+ return diagnostics;
177
+ },
178
+ };
@@ -0,0 +1,129 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noRedundantTypeImportRule } from "./no-redundant-type-import";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR012: no-redundant-type-import", () => {
12
+ test("rule metadata", () => {
13
+ expect(noRedundantTypeImportRule.id).toBe("COR012");
14
+ expect(noRedundantTypeImportRule.severity).toBe("warning");
15
+ expect(noRedundantTypeImportRule.category).toBe("style");
16
+ });
17
+
18
+ test("flags redundant type import alongside namespace import", () => {
19
+ const ctx = createContext(
20
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { PolicyDocument } from "@intentius/chant-lexicon-testdom";`
21
+ );
22
+ const diags = noRedundantTypeImportRule.check(ctx);
23
+ expect(diags).toHaveLength(1);
24
+ expect(diags[0].ruleId).toBe("COR012");
25
+ expect(diags[0].message).toContain("td.PolicyDocument");
26
+ expect(diags[0].message).toContain("Redundant type import");
27
+ });
28
+
29
+ test("flags multiple type names in one import", () => {
30
+ const ctx = createContext(
31
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { PolicyDocument, Code, Environment } from "@intentius/chant-lexicon-testdom";`
32
+ );
33
+ const diags = noRedundantTypeImportRule.check(ctx);
34
+ expect(diags).toHaveLength(1);
35
+ expect(diags[0].message).toContain("td.PolicyDocument");
36
+ expect(diags[0].message).toContain("td.Code");
37
+ expect(diags[0].message).toContain("td.Environment");
38
+ });
39
+
40
+ test("OK: type import alone without namespace import", () => {
41
+ const ctx = createContext(
42
+ `import type { PolicyDocument } from "@intentius/chant-lexicon-testdom";`
43
+ );
44
+ const diags = noRedundantTypeImportRule.check(ctx);
45
+ expect(diags).toHaveLength(0);
46
+ });
47
+
48
+ test("OK: namespace import alone without type import", () => {
49
+ const ctx = createContext(`import * as td from "@intentius/chant-lexicon-testdom";`);
50
+ const diags = noRedundantTypeImportRule.check(ctx);
51
+ expect(diags).toHaveLength(0);
52
+ });
53
+
54
+ test("OK: type import from non-@intentius/chant package", () => {
55
+ const ctx = createContext(
56
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { Foo } from "some-other-package";`
57
+ );
58
+ const diags = noRedundantTypeImportRule.check(ctx);
59
+ expect(diags).toHaveLength(0);
60
+ });
61
+
62
+ test("OK: type import from different @intentius/chant package than namespace", () => {
63
+ const ctx = createContext(
64
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { Foo } from "@intentius/chant";`
65
+ );
66
+ const diags = noRedundantTypeImportRule.check(ctx);
67
+ expect(diags).toHaveLength(0);
68
+ });
69
+
70
+ test("flags both when two packages each have namespace + type imports", () => {
71
+ const ctx = createContext(
72
+ [
73
+ `import * as td from "@intentius/chant-lexicon-testdom";`,
74
+ `import * as core from "@intentius/chant";`,
75
+ `import type { PolicyDocument } from "@intentius/chant-lexicon-testdom";`,
76
+ `import type { Entity } from "@intentius/chant";`,
77
+ ].join("\n")
78
+ );
79
+ const diags = noRedundantTypeImportRule.check(ctx);
80
+ expect(diags).toHaveLength(2);
81
+ expect(diags[0].message).toContain("td.PolicyDocument");
82
+ expect(diags[1].message).toContain("core.Entity");
83
+
84
+ });
85
+
86
+ test("message includes namespace alias and type names", () => {
87
+ const ctx = createContext(
88
+ `import * as myAlias from "@intentius/chant-lexicon-testdom";\nimport type { SomeType } from "@intentius/chant-lexicon-testdom";`
89
+ );
90
+ const diags = noRedundantTypeImportRule.check(ctx);
91
+ expect(diags).toHaveLength(1);
92
+ expect(diags[0].message).toContain("myAlias.SomeType");
93
+ });
94
+
95
+ test("fix range covers the import line", () => {
96
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { PolicyDocument } from "@intentius/chant-lexicon-testdom";\n`;
97
+ const ctx = createContext(code);
98
+ const diags = noRedundantTypeImportRule.check(ctx);
99
+ expect(diags).toHaveLength(1);
100
+ expect(diags[0].fix).toBeDefined();
101
+ const fix = diags[0].fix!;
102
+ expect(fix.replacement).toBe("");
103
+ // Fix should cover the type import line
104
+ const removed = code.slice(fix.range[0], fix.range[1]);
105
+ expect(removed).toContain("import type");
106
+ expect(removed).toContain("PolicyDocument");
107
+ // Should not remove the namespace import
108
+ expect(removed).not.toContain("import * as aws");
109
+ });
110
+
111
+ test("does not flag non-type-only named imports", () => {
112
+ const ctx = createContext(
113
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport { PolicyDocument } from "@intentius/chant-lexicon-testdom";`
114
+ );
115
+ const diags = noRedundantTypeImportRule.check(ctx);
116
+ expect(diags).toHaveLength(0);
117
+ });
118
+
119
+ test("diagnostic has correct line and column", () => {
120
+ const ctx = createContext(
121
+ `import * as td from "@intentius/chant-lexicon-testdom";\nimport type { PolicyDocument } from "@intentius/chant-lexicon-testdom";`
122
+ );
123
+ const diags = noRedundantTypeImportRule.check(ctx);
124
+ expect(diags).toHaveLength(1);
125
+ expect(diags[0].line).toBe(2);
126
+ expect(diags[0].column).toBeGreaterThan(0);
127
+ expect(diags[0].file).toBe("test.ts");
128
+ });
129
+ });
@@ -0,0 +1,85 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR012: no-redundant-type-import
6
+ *
7
+ * Flag `import type { X } from "@intentius/chant-pkg"` when a namespace import
8
+ * `import * as pkg from "@intentius/chant-pkg"` already exists — the type is
9
+ * accessible as `pkg.X`.
10
+ */
11
+
12
+ export const noRedundantTypeImportRule: LintRule = {
13
+ id: "COR012",
14
+ severity: "warning",
15
+ category: "style",
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const diagnostics: LintDiagnostic[] = [];
18
+ const sf = context.sourceFile;
19
+
20
+ // Pass 1: collect namespace imports from @intentius/chant* packages
21
+ // Map: module specifier → namespace alias
22
+ const namespaceImports = new Map<string, string>();
23
+
24
+ for (const stmt of sf.statements) {
25
+ if (
26
+ ts.isImportDeclaration(stmt) &&
27
+ ts.isStringLiteral(stmt.moduleSpecifier)
28
+ ) {
29
+ const modulePath = stmt.moduleSpecifier.text;
30
+ if (!modulePath.startsWith("@intentius/chant") && !modulePath.startsWith("@intentius/chant-lexicon-")) continue;
31
+ const clause = stmt.importClause;
32
+ if (clause && clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
33
+ namespaceImports.set(modulePath, clause.namedBindings.name.text);
34
+ }
35
+ }
36
+ }
37
+
38
+ if (namespaceImports.size === 0) return diagnostics;
39
+
40
+ // Pass 2: find type-only named imports from the same modules
41
+ for (const stmt of sf.statements) {
42
+ if (
43
+ ts.isImportDeclaration(stmt) &&
44
+ ts.isStringLiteral(stmt.moduleSpecifier)
45
+ ) {
46
+ const modulePath = stmt.moduleSpecifier.text;
47
+ const alias = namespaceImports.get(modulePath);
48
+ if (alias === undefined) continue;
49
+
50
+ const clause = stmt.importClause;
51
+ if (!clause || !clause.isTypeOnly) continue;
52
+ if (!clause.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue;
53
+
54
+ const typeNames = clause.namedBindings.elements.map(e => e.name.text);
55
+ const qualifiedNames = typeNames.map(n => `${alias}.${n}`).join(", ");
56
+
57
+ const { line, character } = sf.getLineAndCharacterOfPosition(stmt.getStart(sf));
58
+
59
+ // Fix: remove the entire import line (including trailing newline)
60
+ const start = stmt.getFullStart();
61
+ const end = stmt.getEnd();
62
+ // Extend past trailing newline if present
63
+ const fullText = sf.getFullText();
64
+ let fixEnd = end;
65
+ if (fullText[fixEnd] === "\n") fixEnd++;
66
+ else if (fullText[fixEnd] === "\r" && fullText[fixEnd + 1] === "\n") fixEnd += 2;
67
+
68
+ diagnostics.push({
69
+ file: context.filePath,
70
+ line: line + 1,
71
+ column: character + 1,
72
+ ruleId: "COR012",
73
+ severity: "warning",
74
+ message: `Redundant type import — use ${qualifiedNames} instead. The namespace import already provides access to all exported types.`,
75
+ fix: {
76
+ range: [start, fixEnd],
77
+ replacement: "",
78
+ },
79
+ });
80
+ }
81
+ }
82
+
83
+ return diagnostics;
84
+ },
85
+ };
@@ -0,0 +1,51 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noRedundantValueCastRule } from "./no-redundant-value-cast";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR015: no-redundant-value-cast", () => {
12
+ test("rule metadata", () => {
13
+ expect(noRedundantValueCastRule.id).toBe("COR015");
14
+ expect(noRedundantValueCastRule.severity).toBe("warning");
15
+ expect(noRedundantValueCastRule.category).toBe("style");
16
+ });
17
+
18
+ test("flags as Value<string> cast", () => {
19
+ const ctx = createContext(`const x = role.arn as Value<string>;`);
20
+ const diags = noRedundantValueCastRule.check(ctx);
21
+ expect(diags).toHaveLength(1);
22
+ expect(diags[0].ruleId).toBe("COR015");
23
+ expect(diags[0].message).toContain("Redundant");
24
+ });
25
+
26
+ test("flags as Value<number> cast", () => {
27
+ const ctx = createContext(`const x = something as Value<number>;`);
28
+ const diags = noRedundantValueCastRule.check(ctx);
29
+ expect(diags).toHaveLength(1);
30
+ });
31
+
32
+ test("does not flag other as casts", () => {
33
+ const ctx = createContext(`const x = something as string;`);
34
+ const diags = noRedundantValueCastRule.check(ctx);
35
+ expect(diags).toHaveLength(0);
36
+ });
37
+
38
+ test("does not flag when no cast is used", () => {
39
+ const ctx = createContext(`const x = role.arn;`);
40
+ const diags = noRedundantValueCastRule.check(ctx);
41
+ expect(diags).toHaveLength(0);
42
+ });
43
+
44
+ test("flags multiple casts in one file", () => {
45
+ const ctx = createContext(
46
+ `const a = x.arn as Value<string>;\nconst b = y.id as Value<string>;`
47
+ );
48
+ const diags = noRedundantValueCastRule.check(ctx);
49
+ expect(diags).toHaveLength(2);
50
+ });
51
+ });