@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,46 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR015: no-redundant-value-cast
6
+ *
7
+ * Flag `as Value<...>` type assertions — AttrRef already implements Intrinsic,
8
+ * so it satisfies Value<T> without a cast.
9
+ *
10
+ * Triggers on: role.arn as Value<string>
11
+ * OK: role.arn (used directly)
12
+ */
13
+
14
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
15
+ if (ts.isAsExpression(node)) {
16
+ const typeNode = node.type;
17
+ if (ts.isTypeReferenceNode(typeNode)) {
18
+ const typeName = typeNode.typeName;
19
+ if (ts.isIdentifier(typeName) && typeName.text === "Value") {
20
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
21
+ node.getStart(context.sourceFile)
22
+ );
23
+ diagnostics.push({
24
+ file: context.filePath,
25
+ line: line + 1,
26
+ column: character + 1,
27
+ ruleId: "COR015",
28
+ severity: "warning",
29
+ message: `Redundant 'as Value<...>' cast — AttrRef and other Intrinsic types already satisfy Value<T>.`,
30
+ });
31
+ }
32
+ }
33
+ }
34
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
35
+ }
36
+
37
+ export const noRedundantValueCastRule: LintRule = {
38
+ id: "COR015",
39
+ severity: "warning",
40
+ category: "style",
41
+ check(context: LintContext): LintDiagnostic[] {
42
+ const diagnostics: LintDiagnostic[] = [];
43
+ checkNode(context.sourceFile, context, diagnostics);
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,100 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noStringRefRule } from "./no-string-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("COR003: no-string-ref", () => {
12
+ test("rule metadata", () => {
13
+ expect(noStringRefRule.id).toBe("COR003");
14
+ expect(noStringRefRule.severity).toBe("warning");
15
+ expect(noStringRefRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("flags td.GetAtt()", () => {
19
+ const ctx = createContext(`const arn = td.GetAtt("myBucket", "Arn");`);
20
+ const diags = noStringRefRule.check(ctx);
21
+ expect(diags).toHaveLength(1);
22
+ expect(diags[0].ruleId).toBe("COR003");
23
+ expect(diags[0].message).toContain("GetAtt");
24
+ });
25
+
26
+ test("flags td.Ref()", () => {
27
+ const ctx = createContext(`const id = td.Ref("myBucket");`);
28
+ const diags = noStringRefRule.check(ctx);
29
+ expect(diags).toHaveLength(1);
30
+ expect(diags[0].ruleId).toBe("COR003");
31
+ expect(diags[0].message).toContain("Ref");
32
+ });
33
+
34
+ test("flags bare GetAtt()", () => {
35
+ const ctx = createContext(`const arn = GetAtt("myBucket", "Arn");`);
36
+ const diags = noStringRefRule.check(ctx);
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].ruleId).toBe("COR003");
39
+ expect(diags[0].message).toContain("GetAtt");
40
+ });
41
+
42
+ test("flags bare Ref()", () => {
43
+ const ctx = createContext(`const id = Ref("myBucket");`);
44
+ const diags = noStringRefRule.check(ctx);
45
+ expect(diags).toHaveLength(1);
46
+ expect(diags[0].ruleId).toBe("COR003");
47
+ expect(diags[0].message).toContain("Ref");
48
+ });
49
+
50
+ test("allows AttrRef property access", () => {
51
+ const ctx = createContext(`const arn = bucket.arn;`);
52
+ const diags = noStringRefRule.check(ctx);
53
+ expect(diags).toHaveLength(0);
54
+ });
55
+
56
+ test("allows resource constructor", () => {
57
+ const ctx = createContext(`const bucket = new td.Bucket({ bucketName: "my-bucket" });`);
58
+ const diags = noStringRefRule.check(ctx);
59
+ expect(diags).toHaveLength(0);
60
+ });
61
+
62
+ test("allows Sub/If/Join intrinsics", () => {
63
+ const ctx = createContext(
64
+ [
65
+ `const a = td.Sub("arn:\${TestDom::AccountId}");`,
66
+ `const b = td.If("cond", "a", "b");`,
67
+ `const c = td.Join("-", ["a", "b"]);`,
68
+ ].join("\n")
69
+ );
70
+ const diags = noStringRefRule.check(ctx);
71
+ expect(diags).toHaveLength(0);
72
+ });
73
+
74
+ test("multiple violations", () => {
75
+ const ctx = createContext(
76
+ [
77
+ `const a = GetAtt("bucket1", "Arn");`,
78
+ `const b = td.GetAtt("bucket2", "DomainName");`,
79
+ `const c = GetAtt("bucket3", "WebsiteURL");`,
80
+ ].join("\n")
81
+ );
82
+ const diags = noStringRefRule.check(ctx);
83
+ expect(diags).toHaveLength(3);
84
+ expect(diags.every((d) => d.ruleId === "COR003")).toBe(true);
85
+ });
86
+
87
+ test("correct line and column", () => {
88
+ const code = [
89
+ `const x = 1;`,
90
+ `const arn = GetAtt("myBucket", "Arn");`,
91
+ `const y = 2;`,
92
+ ].join("\n");
93
+ const ctx = createContext(code);
94
+ const diags = noStringRefRule.check(ctx);
95
+ expect(diags).toHaveLength(1);
96
+ expect(diags[0].line).toBe(2);
97
+ expect(diags[0].column).toBe(13);
98
+ expect(diags[0].file).toBe("test.ts");
99
+ });
100
+ });
@@ -0,0 +1,66 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR003: no-string-ref
6
+ *
7
+ * Flag string-based `GetAtt("name", "attr")` and `Ref("name")` intrinsic
8
+ * calls. These bypass type safety — users should import resources and use
9
+ * typed AttrRef properties like `resource.arn`.
10
+ */
11
+
12
+ const TARGET_FUNCTIONS = new Set(["GetAtt", "Ref"]);
13
+
14
+ export const noStringRefRule: LintRule = {
15
+ id: "COR003",
16
+ severity: "warning",
17
+ category: "correctness",
18
+ check(context: LintContext): LintDiagnostic[] {
19
+ const diagnostics: LintDiagnostic[] = [];
20
+ const sf = context.sourceFile;
21
+
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isCallExpression(node)) {
24
+ const name = getCalledFunctionName(node);
25
+ if (name && TARGET_FUNCTIONS.has(name) && hasStringArguments(node)) {
26
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
27
+ const fnText = name === "GetAtt" ? "GetAtt" : "Ref";
28
+ diagnostics.push({
29
+ file: context.filePath,
30
+ line: line + 1,
31
+ column: character + 1,
32
+ ruleId: "COR003",
33
+ severity: "warning",
34
+ message: `Avoid string-based ${fnText}() — import the resource and use typed property access instead.`,
35
+ });
36
+ }
37
+ }
38
+ ts.forEachChild(node, visit);
39
+ }
40
+
41
+ visit(sf);
42
+ return diagnostics;
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Extract the function name from a call expression.
48
+ * Handles both `GetAtt(...)` and `aws.GetAtt(...)` patterns.
49
+ */
50
+ function getCalledFunctionName(node: ts.CallExpression): string | undefined {
51
+ const expr = node.expression;
52
+ if (ts.isIdentifier(expr)) {
53
+ return expr.text;
54
+ }
55
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
56
+ return expr.name.text;
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ /**
62
+ * Check if the call has at least one string literal argument.
63
+ */
64
+ function hasStringArguments(node: ts.CallExpression): boolean {
65
+ return node.arguments.some((arg) => ts.isStringLiteral(arg));
66
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noUnusedDeclarableImportRule } from "./no-unused-declarable-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("COR010: no-unused-declarable-import", () => {
12
+ test("rule metadata", () => {
13
+ expect(noUnusedDeclarableImportRule.id).toBe("COR010");
14
+ expect(noUnusedDeclarableImportRule.severity).toBe("warning");
15
+ expect(noUnusedDeclarableImportRule.category).toBe("style");
16
+ });
17
+
18
+ test("triggers on unused namespace import", () => {
19
+ const ctx = createContext(`import * as td from "@intentius/chant-lexicon-testdom";\nexport const x = 1;`);
20
+ const diags = noUnusedDeclarableImportRule.check(ctx);
21
+ expect(diags).toHaveLength(1);
22
+ expect(diags[0].ruleId).toBe("COR010");
23
+ expect(diags[0].message).toContain("td");
24
+ });
25
+
26
+ test("does not trigger when namespace is used as property access", () => {
27
+ const ctx = createContext(
28
+ `import * as td from "@intentius/chant-lexicon-testdom";\nexport const b = new td.Bucket({});`,
29
+ );
30
+ const diags = noUnusedDeclarableImportRule.check(ctx);
31
+ expect(diags).toHaveLength(0);
32
+ });
33
+
34
+ test("does not trigger on type-only imports", () => {
35
+ const ctx = createContext(
36
+ `import type * as core from "@intentius/chant";\nexport const x = 1;`,
37
+ );
38
+ const diags = noUnusedDeclarableImportRule.check(ctx);
39
+ expect(diags).toHaveLength(0);
40
+ });
41
+
42
+ test("does not trigger on non-chant imports", () => {
43
+ const ctx = createContext(`import * as ts from "typescript";\nexport const x = 1;`);
44
+ const diags = noUnusedDeclarableImportRule.check(ctx);
45
+ expect(diags).toHaveLength(0);
46
+ });
47
+
48
+ test("does not trigger on named imports (those are COR006)", () => {
49
+ const ctx = createContext(`import { Bucket } from "@intentius/chant-lexicon-testdom";\nexport const x = 1;`);
50
+ const diags = noUnusedDeclarableImportRule.check(ctx);
51
+ expect(diags).toHaveLength(0);
52
+ });
53
+
54
+ test("handles multiple namespace imports", () => {
55
+ const ctx = createContext(
56
+ `import * as td from "@intentius/chant-lexicon-testdom";\n` +
57
+ `import * as core from "@intentius/chant";\n` +
58
+ `export const b = new td.Bucket({});`,
59
+ );
60
+ const diags = noUnusedDeclarableImportRule.check(ctx);
61
+ // aws is used, core is not
62
+ expect(diags).toHaveLength(1);
63
+ expect(diags[0].message).toContain("core");
64
+ });
65
+
66
+ test("detects usage in type positions (qualified names)", () => {
67
+ const ctx = createContext(
68
+ `import * as core from "@intentius/chant";\n` +
69
+ `const x: core.Value<string> = "hello";`,
70
+ );
71
+ const diags = noUnusedDeclarableImportRule.check(ctx);
72
+ expect(diags).toHaveLength(0);
73
+ });
74
+ });
@@ -0,0 +1,103 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR010: no-unused-declarable-import
6
+ *
7
+ * Namespace imports from @intentius/chant* must be referenced.
8
+ *
9
+ * Triggers on: import * as <name> from "@intentius/chant-lexicon-<name>" (when <name>. never appears)
10
+ * OK: import * as <name> from "@intentius/chant-lexicon-<name>" (when <name>.Bucket is used)
11
+ * OK: import type * as core from "@intentius/chant" (type-only)
12
+ */
13
+
14
+ interface NamespaceImportInfo {
15
+ name: string;
16
+ node: ts.ImportDeclaration;
17
+ }
18
+
19
+ function collectNamespaceImports(sourceFile: ts.SourceFile): NamespaceImportInfo[] {
20
+ const imports: NamespaceImportInfo[] = [];
21
+
22
+ ts.forEachChild(sourceFile, (node) => {
23
+ if (!ts.isImportDeclaration(node)) return;
24
+
25
+ const moduleSpecifier = node.moduleSpecifier;
26
+ if (!ts.isStringLiteral(moduleSpecifier)) return;
27
+ if (!moduleSpecifier.text.startsWith("@intentius/chant") && !moduleSpecifier.text.startsWith("@intentius/chant-lexicon-")) return;
28
+
29
+ // Skip type-only imports
30
+ if (node.importClause?.isTypeOnly) return;
31
+
32
+ const importClause = node.importClause;
33
+ if (!importClause?.namedBindings) return;
34
+
35
+ // Only check namespace imports (import * as X)
36
+ if (ts.isNamespaceImport(importClause.namedBindings)) {
37
+ imports.push({
38
+ name: importClause.namedBindings.name.text,
39
+ node,
40
+ });
41
+ }
42
+ });
43
+
44
+ return imports;
45
+ }
46
+
47
+ function isNamespaceUsed(name: string, sourceFile: ts.SourceFile): boolean {
48
+ let used = false;
49
+
50
+ function visit(node: ts.Node): void {
51
+ if (used) return;
52
+
53
+ // Check for property access: name.something
54
+ if (ts.isPropertyAccessExpression(node)) {
55
+ if (ts.isIdentifier(node.expression) && node.expression.text === name) {
56
+ used = true;
57
+ return;
58
+ }
59
+ }
60
+
61
+ // Check for qualified names in types: name.Type
62
+ if (ts.isQualifiedName(node)) {
63
+ if (ts.isIdentifier(node.left) && node.left.text === name) {
64
+ used = true;
65
+ return;
66
+ }
67
+ }
68
+
69
+ ts.forEachChild(node, visit);
70
+ }
71
+
72
+ visit(sourceFile);
73
+ return used;
74
+ }
75
+
76
+ export const noUnusedDeclarableImportRule: LintRule = {
77
+ id: "COR010",
78
+ severity: "warning",
79
+ category: "style",
80
+ check(context: LintContext): LintDiagnostic[] {
81
+ const diagnostics: LintDiagnostic[] = [];
82
+ const imports = collectNamespaceImports(context.sourceFile);
83
+
84
+ for (const imp of imports) {
85
+ if (!isNamespaceUsed(imp.name, context.sourceFile)) {
86
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
87
+ imp.node.getStart(context.sourceFile),
88
+ );
89
+
90
+ diagnostics.push({
91
+ file: context.filePath,
92
+ line: line + 1,
93
+ column: character + 1,
94
+ ruleId: "COR010",
95
+ severity: "warning",
96
+ message: `Namespace import '${imp.name}' is never used — remove the import or use ${imp.name}.<resource>.`,
97
+ });
98
+ }
99
+ }
100
+
101
+ return diagnostics;
102
+ },
103
+ };
@@ -0,0 +1,134 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { noUnusedDeclarableRule } from "./no-unused-declarable";
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("COR004: no-unused-declarable", () => {
12
+ test("rule metadata", () => {
13
+ expect(noUnusedDeclarableRule.id).toBe("COR004");
14
+ expect(noUnusedDeclarableRule.severity).toBe("warning");
15
+ expect(noUnusedDeclarableRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("flags exported declarable that is never referenced", () => {
19
+ const ctx = createContext(`export const bucket = new Bucket({ bucketName: "x" });`);
20
+ const diags = noUnusedDeclarableRule.check(ctx);
21
+ expect(diags).toHaveLength(1);
22
+ expect(diags[0].ruleId).toBe("COR004");
23
+ expect(diags[0].severity).toBe("warning");
24
+ expect(diags[0].message).toContain("bucket");
25
+ expect(diags[0].message).toContain("never referenced");
26
+ });
27
+
28
+ test("does not flag declarable that is referenced by another declarable", () => {
29
+ const ctx = createContext(
30
+ `export const bucket = new Bucket({ bucketName: "x" });\n` +
31
+ `export const fn = new Function({ bucket: bucket.arn });`,
32
+ );
33
+ const diags = noUnusedDeclarableRule.check(ctx);
34
+ // bucket is referenced by fn, but fn itself is unreferenced
35
+ expect(diags).toHaveLength(1);
36
+ expect(diags[0].message).toContain("fn");
37
+ });
38
+
39
+ test("does not flag declarable referenced via namespace import style", () => {
40
+ const ctx = createContext(
41
+ `import * as td from "@intentius/chant-lexicon-testdom";\n` +
42
+ `export const bucket = new td.Bucket({ bucketName: "x" });\n` +
43
+ `export const fn = new td.Function({ env: { BUCKET: bucket.arn } });`,
44
+ );
45
+ const diags = noUnusedDeclarableRule.check(ctx);
46
+ // bucket is referenced, fn is not
47
+ expect(diags).toHaveLength(1);
48
+ expect(diags[0].message).toContain("fn");
49
+ });
50
+
51
+ test("OK for non-declarable exports (plain objects, functions)", () => {
52
+ const ctx = createContext(
53
+ `export const config = { region: "us-east-1" };\n` +
54
+ `export function helper() { return 1; }`,
55
+ );
56
+ const diags = noUnusedDeclarableRule.check(ctx);
57
+ expect(diags).toHaveLength(0);
58
+ });
59
+
60
+ test("OK for non-exported declarable (that's COR008's job)", () => {
61
+ const ctx = createContext(`const bucket = new Bucket({ bucketName: "x" });`);
62
+ const diags = noUnusedDeclarableRule.check(ctx);
63
+ expect(diags).toHaveLength(0);
64
+ });
65
+
66
+ test("OK for new expression with lowercase name (not a declarable)", () => {
67
+ const ctx = createContext(`export const obj = new someFactory();`);
68
+ const diags = noUnusedDeclarableRule.check(ctx);
69
+ expect(diags).toHaveLength(0);
70
+ });
71
+
72
+ test("flags multiple unused declarables", () => {
73
+ const ctx = createContext(
74
+ `export const bucket = new Bucket({ bucketName: "x" });\n` +
75
+ `export const role = new Role({ roleName: "y" });\n` +
76
+ `export const fn = new Function({});`,
77
+ );
78
+ const diags = noUnusedDeclarableRule.check(ctx);
79
+ expect(diags).toHaveLength(3);
80
+ const names = diags.map((d) => d.message);
81
+ expect(names.some((m) => m.includes("bucket"))).toBe(true);
82
+ expect(names.some((m) => m.includes("role"))).toBe(true);
83
+ expect(names.some((m) => m.includes("fn"))).toBe(true);
84
+ });
85
+
86
+ test("flags only unused declarables when some are referenced", () => {
87
+ const ctx = createContext(
88
+ `export const bucket = new Bucket({ bucketName: "x" });\n` +
89
+ `export const role = new Role({ roleName: "y" });\n` +
90
+ `export const fn = new Function({ bucket: bucket.arn, role: role.arn });`,
91
+ );
92
+ const diags = noUnusedDeclarableRule.check(ctx);
93
+ // bucket and role are referenced by fn, but fn is unreferenced
94
+ expect(diags).toHaveLength(1);
95
+ expect(diags[0].message).toContain("fn");
96
+ });
97
+
98
+ test("reports correct file path", () => {
99
+ const ctx = createContext(
100
+ `export const bucket = new Bucket({ bucketName: "x" });`,
101
+ "infra/storage.ts",
102
+ );
103
+ const diags = noUnusedDeclarableRule.check(ctx);
104
+ expect(diags).toHaveLength(1);
105
+ expect(diags[0].file).toBe("infra/storage.ts");
106
+ });
107
+
108
+ test("reports correct line and column", () => {
109
+ const ctx = createContext(
110
+ `import * as td from "@intentius/chant-lexicon-testdom";\n` +
111
+ `export const bucket = new td.Bucket({ bucketName: "x" });`,
112
+ );
113
+ const diags = noUnusedDeclarableRule.check(ctx);
114
+ expect(diags).toHaveLength(1);
115
+ expect(diags[0].line).toBe(2);
116
+ expect(diags[0].column).toBe(1);
117
+ });
118
+
119
+ test("detects reference as function argument", () => {
120
+ const ctx = createContext(
121
+ `export const bucket = new Bucket({});\n` + `console.log(bucket);`,
122
+ );
123
+ const diags = noUnusedDeclarableRule.check(ctx);
124
+ expect(diags).toHaveLength(0);
125
+ });
126
+
127
+ test("detects reference in array literal", () => {
128
+ const ctx = createContext(
129
+ `export const bucket = new Bucket({});\n` + `export const list = [bucket];`,
130
+ );
131
+ const diags = noUnusedDeclarableRule.check(ctx);
132
+ expect(diags).toHaveLength(0);
133
+ });
134
+ });
@@ -0,0 +1,118 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR004: no-unused-declarable
6
+ *
7
+ * Detects exported declarables (export const x = new X(...)) that are never
8
+ * referenced elsewhere in the same file. This catches orphaned infrastructure
9
+ * declarations that nothing depends on.
10
+ *
11
+ * Triggers on: export const bucket = new Bucket({...}) when bucket is never referenced
12
+ * OK: export const bucket = new Bucket({...}); export const fn = new Function({ bucket: bucket.arn })
13
+ */
14
+
15
+ interface DeclarableInfo {
16
+ name: string;
17
+ node: ts.VariableStatement;
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
+ // Must have export modifier
41
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
42
+ if (!hasExport) return;
43
+
44
+ for (const decl of node.declarationList.declarations) {
45
+ if (!ts.isIdentifier(decl.name)) continue;
46
+ if (!decl.initializer) continue;
47
+
48
+ // Must be a new expression with a capitalized name
49
+ if (!ts.isNewExpression(decl.initializer)) continue;
50
+
51
+ const className = getNewExpressionClassName(decl.initializer);
52
+ if (!className || !isCapitalized(className)) continue;
53
+
54
+ declarables.push({
55
+ name: decl.name.text,
56
+ node,
57
+ });
58
+ }
59
+ });
60
+
61
+ return declarables;
62
+ }
63
+
64
+ function collectReferences(name: string, sourceFile: ts.SourceFile, declarationNode: ts.Node): boolean {
65
+ let found = false;
66
+
67
+ function visit(node: ts.Node): void {
68
+ if (found) return;
69
+
70
+ // Skip the declaration itself
71
+ if (node === declarationNode) return;
72
+
73
+ if (ts.isIdentifier(node) && node.text === name) {
74
+ // Make sure this isn't the declaration's own name
75
+ const parent = node.parent;
76
+ if (parent && ts.isVariableDeclaration(parent) && parent.name === node) {
77
+ // This is the declaration itself, skip
78
+ } else {
79
+ found = true;
80
+ return;
81
+ }
82
+ }
83
+
84
+ ts.forEachChild(node, visit);
85
+ }
86
+
87
+ visit(sourceFile);
88
+ return found;
89
+ }
90
+
91
+ export const noUnusedDeclarableRule: LintRule = {
92
+ id: "COR004",
93
+ severity: "warning",
94
+ category: "correctness",
95
+ check(context: LintContext): LintDiagnostic[] {
96
+ const diagnostics: LintDiagnostic[] = [];
97
+ const declarables = collectExportedDeclarables(context.sourceFile);
98
+
99
+ for (const decl of declarables) {
100
+ if (!collectReferences(decl.name, context.sourceFile, decl.node)) {
101
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
102
+ decl.node.getStart(context.sourceFile),
103
+ );
104
+
105
+ diagnostics.push({
106
+ file: context.filePath,
107
+ line: line + 1,
108
+ column: character + 1,
109
+ ruleId: "COR004",
110
+ severity: "warning",
111
+ message: `Exported declarable '${decl.name}' is never referenced in this file — it may be dead infrastructure code.`,
112
+ });
113
+ }
114
+ }
115
+
116
+ return diagnostics;
117
+ },
118
+ };