@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,102 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { preferNamespaceImportRule } from "./prefer-namespace-import";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(
8
+ filePath,
9
+ code,
10
+ ts.ScriptTarget.Latest,
11
+ true
12
+ );
13
+
14
+ return {
15
+ sourceFile,
16
+ entities: [],
17
+ filePath,
18
+ lexicon: undefined,
19
+ };
20
+ }
21
+
22
+ describe("COR006: prefer-namespace-import", () => {
23
+ test("rule metadata", () => {
24
+ expect(preferNamespaceImportRule.id).toBe("COR006");
25
+ expect(preferNamespaceImportRule.severity).toBe("error");
26
+ expect(preferNamespaceImportRule.category).toBe("style");
27
+ });
28
+
29
+ test("flags named import from @intentius/chant* package", () => {
30
+ const code = `import { Bucket } from "@intentius/chant-lexicon-testdom";`;
31
+ const context = createContext(code);
32
+ const diagnostics = preferNamespaceImportRule.check(context);
33
+
34
+ expect(diagnostics).toHaveLength(1);
35
+ expect(diagnostics[0].ruleId).toBe("COR006");
36
+ expect(diagnostics[0].severity).toBe("error");
37
+ expect(diagnostics[0].message).toBe(
38
+ `Use namespace import for @intentius/chant-lexicon-testdom — replace with: import * as testdom from "@intentius/chant-lexicon-testdom"`
39
+ );
40
+ });
41
+
42
+ test("allows namespace import from @intentius/chant* package", () => {
43
+ const code = `import * as testdom from "@intentius/chant-lexicon-testdom";`;
44
+ const context = createContext(code);
45
+ const diagnostics = preferNamespaceImportRule.check(context);
46
+ expect(diagnostics).toHaveLength(0);
47
+ });
48
+
49
+ test("allows type-only import from @intentius/chant* package", () => {
50
+ const code = `import type { Declarable } from "@intentius/chant";`;
51
+ const context = createContext(code);
52
+ const diagnostics = preferNamespaceImportRule.check(context);
53
+ expect(diagnostics).toHaveLength(0);
54
+ });
55
+
56
+ test("does not flag imports from non-@intentius/chant packages", () => {
57
+ const code = `import { useState } from "react";`;
58
+ const context = createContext(code);
59
+ const diagnostics = preferNamespaceImportRule.check(context);
60
+ expect(diagnostics).toHaveLength(0);
61
+ });
62
+
63
+ test("does not flag imports from relative paths", () => {
64
+ const code = `import { helper } from "./utils";`;
65
+ const context = createContext(code);
66
+ const diagnostics = preferNamespaceImportRule.check(context);
67
+ expect(diagnostics).toHaveLength(0);
68
+ });
69
+
70
+ test("derives alias from package name", () => {
71
+ const code = `import { Config } from "@intentius/chant";`;
72
+ const context = createContext(code);
73
+ const diagnostics = preferNamespaceImportRule.check(context);
74
+
75
+ expect(diagnostics).toHaveLength(1);
76
+ expect(diagnostics[0].message).toContain("import * as core");
77
+ });
78
+
79
+ test("flags multiple named imports from different @intentius/chant packages", () => {
80
+ const code = `
81
+ import { Bucket } from "@intentius/chant-lexicon-testdom";
82
+ import { Config } from "@intentius/chant";
83
+ `;
84
+ const context = createContext(code);
85
+ const diagnostics = preferNamespaceImportRule.check(context);
86
+
87
+ expect(diagnostics).toHaveLength(2);
88
+ expect(diagnostics[0].message).toContain("@intentius/chant-lexicon-testdom");
89
+ expect(diagnostics[1].message).toContain("@intentius/chant");
90
+ });
91
+
92
+ test("reports correct line and column numbers", () => {
93
+ const code = `import { Bucket } from "@intentius/chant-lexicon-testdom";`;
94
+ const context = createContext(code);
95
+ const diagnostics = preferNamespaceImportRule.check(context);
96
+
97
+ expect(diagnostics).toHaveLength(1);
98
+ expect(diagnostics[0].line).toBe(1);
99
+ expect(diagnostics[0].column).toBe(1);
100
+ expect(diagnostics[0].file).toBe("test.ts");
101
+ });
102
+ });
@@ -0,0 +1,63 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR006: prefer-namespace-import
6
+ *
7
+ * Enforce `import * as pkg` for `@intentius/chant*` package imports.
8
+ *
9
+ * Triggers on: import { Bucket } from "@intentius/chant-lexicon-<name>"
10
+ * OK: import * as <name> from "@intentius/chant-lexicon-<name>"
11
+ * OK: import type { Declarable } from "@intentius/chant"
12
+ */
13
+
14
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
15
+ if (ts.isImportDeclaration(node)) {
16
+ const moduleSpecifier = node.moduleSpecifier;
17
+ if (!ts.isStringLiteral(moduleSpecifier)) return;
18
+
19
+ const modulePath = moduleSpecifier.text;
20
+ if (!modulePath.startsWith("@intentius/chant") && !modulePath.startsWith("@intentius/chant-lexicon-")) return;
21
+
22
+ // Skip type-only imports: import type { X } from "..."
23
+ if (node.importClause?.isTypeOnly) return;
24
+
25
+ const importClause = node.importClause;
26
+ if (!importClause?.namedBindings) return;
27
+
28
+ // Flag named imports (not namespace imports)
29
+ if (ts.isNamedImports(importClause.namedBindings)) {
30
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
31
+ node.getStart(context.sourceFile)
32
+ );
33
+
34
+ const pkgName = modulePath === "@intentius/chant"
35
+ ? "core"
36
+ : modulePath.startsWith("@intentius/chant-lexicon-")
37
+ ? modulePath.replace("@intentius/chant-lexicon-", "")
38
+ : modulePath.replace("@intentius/chant-", "");
39
+
40
+ diagnostics.push({
41
+ file: context.filePath,
42
+ line: line + 1,
43
+ column: character + 1,
44
+ ruleId: "COR006",
45
+ severity: "error",
46
+ message: `Use namespace import for ${modulePath} — replace with: import * as ${pkgName} from "${modulePath}"`,
47
+ });
48
+ }
49
+ }
50
+
51
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
52
+ }
53
+
54
+ export const preferNamespaceImportRule: LintRule = {
55
+ id: "COR006",
56
+ severity: "error",
57
+ category: "style",
58
+ check(context: LintContext): LintDiagnostic[] {
59
+ const diagnostics: LintDiagnostic[] = [];
60
+ checkNode(context.sourceFile, context, diagnostics);
61
+ return diagnostics;
62
+ },
63
+ };
@@ -0,0 +1,156 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { singleConcernFileRule } from "./single-concern-file";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(
8
+ filePath,
9
+ code,
10
+ ts.ScriptTarget.Latest,
11
+ true
12
+ );
13
+
14
+ return {
15
+ sourceFile,
16
+ entities: [],
17
+ filePath,
18
+ lexicon: undefined,
19
+ };
20
+ }
21
+
22
+ describe("COR013: single-concern-file", () => {
23
+ test("rule metadata", () => {
24
+ expect(singleConcernFileRule.id).toBe("COR013");
25
+ expect(singleConcernFileRule.severity).toBe("info");
26
+ expect(singleConcernFileRule.category).toBe("style");
27
+ });
28
+
29
+ test("does not trigger on resource-only file", () => {
30
+ const code = `
31
+ export const bucket = new Bucket({ bucketName: "data" });
32
+ export const table = new Table({ tableName: "users" });
33
+ `;
34
+ const context = createContext(code);
35
+ const diagnostics = singleConcernFileRule.check(context);
36
+ expect(diagnostics).toHaveLength(0);
37
+ });
38
+
39
+ test("does not trigger on config-only file", () => {
40
+ const code = `
41
+ export const encryption = new BucketEncryption({ type: "AES256" });
42
+ export const versioning = new VersioningConfiguration({ status: "Enabled" });
43
+ `;
44
+ const context = createContext(code);
45
+ const diagnostics = singleConcernFileRule.check(context);
46
+ expect(diagnostics).toHaveLength(0);
47
+ });
48
+
49
+ test("triggers on file mixing resource and config declarables", () => {
50
+ const code = `
51
+ export const bucket = new Bucket({ bucketName: "data" });
52
+ export const encryption = new BucketEncryption({ type: "AES256" });
53
+ `;
54
+ const context = createContext(code);
55
+ const diagnostics = singleConcernFileRule.check(context);
56
+
57
+ expect(diagnostics).toHaveLength(1);
58
+ expect(diagnostics[0].ruleId).toBe("COR013");
59
+ expect(diagnostics[0].severity).toBe("info");
60
+ expect(diagnostics[0].message).toContain("mixes resource Declarables with configuration Declarables");
61
+ });
62
+
63
+ test("triggers with qualified class names (e.g. aws.Bucket)", () => {
64
+ const code = `
65
+ export const bucket = new aws.Bucket({ bucketName: "data" });
66
+ export const encryption = new aws.BucketEncryption({ type: "AES256" });
67
+ `;
68
+ const context = createContext(code);
69
+ const diagnostics = singleConcernFileRule.check(context);
70
+
71
+ expect(diagnostics).toHaveLength(1);
72
+ expect(diagnostics[0].ruleId).toBe("COR013");
73
+ });
74
+
75
+ test("does not trigger on file with no new expressions", () => {
76
+ const code = `
77
+ export const name = "hello";
78
+ export function greet() { return name; }
79
+ `;
80
+ const context = createContext(code);
81
+ const diagnostics = singleConcernFileRule.check(context);
82
+ expect(diagnostics).toHaveLength(0);
83
+ });
84
+
85
+ test("does not trigger on file with only lowercase constructors", () => {
86
+ const code = `
87
+ const x = new something();
88
+ `;
89
+ const context = createContext(code);
90
+ const diagnostics = singleConcernFileRule.check(context);
91
+ expect(diagnostics).toHaveLength(0);
92
+ });
93
+
94
+ test("triggers with multiple property-kind patterns", () => {
95
+ const code = `
96
+ export const role = new Role({ roleName: "admin" });
97
+ export const policy = new AccessPolicy({ effect: "Allow" });
98
+ `;
99
+ const context = createContext(code);
100
+ const diagnostics = singleConcernFileRule.check(context);
101
+
102
+ expect(diagnostics).toHaveLength(1);
103
+ expect(diagnostics[0].ruleId).toBe("COR013");
104
+ });
105
+
106
+ test("does not trigger with single new expression", () => {
107
+ const code = `
108
+ export const bucket = new Bucket({ bucketName: "data" });
109
+ `;
110
+ const context = createContext(code);
111
+ const diagnostics = singleConcernFileRule.check(context);
112
+ expect(diagnostics).toHaveLength(0);
113
+ });
114
+
115
+ test("reports correct file path", () => {
116
+ const code = `
117
+ export const bucket = new Bucket({});
118
+ export const enc = new BucketEncryption({});
119
+ `;
120
+ const context = createContext(code, "src/my-file.ts");
121
+ const diagnostics = singleConcernFileRule.check(context);
122
+
123
+ expect(diagnostics).toHaveLength(1);
124
+ expect(diagnostics[0].file).toBe("src/my-file.ts");
125
+ });
126
+
127
+ test("handles Configuration suffix", () => {
128
+ const code = `
129
+ export const bucket = new Bucket({});
130
+ export const config = new VersioningConfiguration({});
131
+ `;
132
+ const context = createContext(code);
133
+ const diagnostics = singleConcernFileRule.check(context);
134
+ expect(diagnostics).toHaveLength(1);
135
+ });
136
+
137
+ test("handles Block suffix", () => {
138
+ const code = `
139
+ export const bucket = new Bucket({});
140
+ export const block = new PublicAccessBlock({});
141
+ `;
142
+ const context = createContext(code);
143
+ const diagnostics = singleConcernFileRule.check(context);
144
+ expect(diagnostics).toHaveLength(1);
145
+ });
146
+
147
+ test("handles Specification suffix", () => {
148
+ const code = `
149
+ export const bucket = new Bucket({});
150
+ export const spec = new ServerSideEncryptionSpecification({});
151
+ `;
152
+ const context = createContext(code);
153
+ const diagnostics = singleConcernFileRule.check(context);
154
+ expect(diagnostics).toHaveLength(1);
155
+ });
156
+ });
@@ -0,0 +1,98 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR013: Single-concern file
6
+ *
7
+ * Advisory rule that flags files mixing resource Declarables (e.g. Bucket,
8
+ * Function, Role) with property/config Declarables (e.g. BucketEncryption,
9
+ * VersioningConfiguration) as `new` expressions.
10
+ *
11
+ * Heuristic: class names containing "Configuration", "Encryption", "Policy",
12
+ * "Rule", "Setting", "Block", "Specification" are considered property-kind.
13
+ * All other capitalized `new` expressions are considered resource-kind.
14
+ *
15
+ * Triggers when a single file has `new` expressions for both kinds.
16
+ */
17
+
18
+ const PROPERTY_KIND_PATTERNS = [
19
+ "Configuration",
20
+ "Encryption",
21
+ "Policy",
22
+ "Rule",
23
+ "Setting",
24
+ "Block",
25
+ "Specification",
26
+ ];
27
+
28
+ function isPropertyKindClass(className: string): boolean {
29
+ return PROPERTY_KIND_PATTERNS.some(pattern => className.includes(pattern));
30
+ }
31
+
32
+ function isResourceKindClass(className: string): boolean {
33
+ return /^[A-Z]/.test(className) && !isPropertyKindClass(className);
34
+ }
35
+
36
+ interface NewExpressionInfo {
37
+ className: string;
38
+ node: ts.NewExpression;
39
+ }
40
+
41
+ function collectNewExpressions(
42
+ node: ts.Node,
43
+ sourceFile: ts.SourceFile,
44
+ results: NewExpressionInfo[],
45
+ ): void {
46
+ if (ts.isNewExpression(node)) {
47
+ const className = node.expression.getText(sourceFile);
48
+ // Only consider capitalized class names (likely Declarable constructors)
49
+ if (/^[A-Z]/.test(className) || /\.[A-Z]/.test(className)) {
50
+ // Extract the simple class name (after any dot for qualified access like aws.Bucket)
51
+ const simpleName = className.includes(".")
52
+ ? className.split(".").pop()!
53
+ : className;
54
+ results.push({ className: simpleName, node });
55
+ }
56
+ }
57
+
58
+ ts.forEachChild(node, child =>
59
+ collectNewExpressions(child, sourceFile, results),
60
+ );
61
+ }
62
+
63
+ export const singleConcernFileRule: LintRule = {
64
+ id: "COR013",
65
+ severity: "info",
66
+ category: "style",
67
+ check(context: LintContext): LintDiagnostic[] {
68
+ const newExpressions: NewExpressionInfo[] = [];
69
+ collectNewExpressions(context.sourceFile, context.sourceFile, newExpressions);
70
+
71
+ const resourceExpressions = newExpressions.filter(e =>
72
+ isResourceKindClass(e.className),
73
+ );
74
+ const propertyExpressions = newExpressions.filter(e =>
75
+ isPropertyKindClass(e.className),
76
+ );
77
+
78
+ // Only flag if the file has both kinds
79
+ if (resourceExpressions.length === 0 || propertyExpressions.length === 0) {
80
+ return [];
81
+ }
82
+
83
+ // Report a single diagnostic at the file level (first line)
84
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(0);
85
+
86
+ return [
87
+ {
88
+ file: context.filePath,
89
+ line: line + 1,
90
+ column: character + 1,
91
+ ruleId: "COR013",
92
+ severity: "info",
93
+ message:
94
+ "COR013: File mixes resource Declarables with configuration Declarables — consider splitting into separate files",
95
+ },
96
+ ];
97
+ },
98
+ };
@@ -0,0 +1,60 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
4
+ import { generateBarrelTypes } from "../../project/sync";
5
+
6
+ /**
7
+ * COR016: stale-barrel-types
8
+ *
9
+ * Checks that `_.d.ts` barrel type declarations are up-to-date with
10
+ * the current project scan. Fires only on `_.ts` barrel files.
11
+ *
12
+ * When --fix is used, the fix writes the expected content to `_.d.ts`.
13
+ */
14
+ export const staleBarrelTypesRule: LintRule = {
15
+ id: "COR016",
16
+ severity: "warning",
17
+ category: "correctness",
18
+ check(context: LintContext): LintDiagnostic[] {
19
+ // Only fire on _.ts barrel files
20
+ if (!context.filePath.endsWith("/_.ts") && context.filePath !== "_.ts") {
21
+ return [];
22
+ }
23
+
24
+ // Requires projectScan
25
+ if (!context.projectScan) return [];
26
+
27
+ const expected = generateBarrelTypes(context.projectScan);
28
+ const dtsPath = join(dirname(context.filePath), "_.d.ts");
29
+
30
+ let existing: string | undefined;
31
+ try {
32
+ existing = readFileSync(dtsPath, "utf-8");
33
+ } catch {
34
+ // File doesn't exist
35
+ }
36
+
37
+ if (existing === expected) return [];
38
+
39
+ const message = existing === undefined
40
+ ? "Missing _.d.ts barrel type declarations. Run 'chant lint --fix' to generate."
41
+ : "Stale _.d.ts barrel type declarations. Run 'chant lint --fix' to regenerate.";
42
+
43
+ return [
44
+ {
45
+ file: context.filePath,
46
+ line: 1,
47
+ column: 1,
48
+ ruleId: "COR016",
49
+ severity: "warning",
50
+ message,
51
+ fix: {
52
+ range: [0, 0],
53
+ replacement: "",
54
+ kind: "write-file",
55
+ params: { path: dtsPath, content: expected },
56
+ },
57
+ },
58
+ ];
59
+ },
60
+ };
@@ -0,0 +1,113 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { resolveSelector, registerSelector, collectNodes } from "./selectors";
4
+
5
+ function parse(code: string): ts.SourceFile {
6
+ return ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
7
+ }
8
+
9
+ describe("selectors", () => {
10
+ describe("collectNodes", () => {
11
+ test("collects matching nodes recursively", () => {
12
+ const sf = parse(`const a = "hello"; const b = "world";`);
13
+ const strings = collectNodes(sf, (node) => ts.isStringLiteral(node));
14
+ expect(strings).toHaveLength(2);
15
+ });
16
+
17
+ test("returns empty for no matches", () => {
18
+ const sf = parse(`const a = 1;`);
19
+ const strings = collectNodes(sf, (node) => ts.isStringLiteral(node));
20
+ expect(strings).toHaveLength(0);
21
+ });
22
+ });
23
+
24
+ describe("built-in selectors", () => {
25
+ test("resource selector matches new expressions", () => {
26
+ const sf = parse(`const b = new Bucket({ name: "test" });`);
27
+ const sel = resolveSelector("resource");
28
+ const nodes = sel(sf);
29
+ expect(nodes).toHaveLength(1);
30
+ expect(ts.isNewExpression(nodes[0])).toBe(true);
31
+ });
32
+
33
+ test("any-resource is alias for resource", () => {
34
+ const sf = parse(`const b = new Bucket({}); const c = new Table({});`);
35
+ const sel = resolveSelector("any-resource");
36
+ expect(sel(sf)).toHaveLength(2);
37
+ });
38
+
39
+ test("string-literal matches string literals", () => {
40
+ const sf = parse(`const a = "hello"; const b = 42;`);
41
+ const sel = resolveSelector("string-literal");
42
+ const nodes = sel(sf);
43
+ expect(nodes).toHaveLength(1);
44
+ expect(ts.isStringLiteral(nodes[0])).toBe(true);
45
+ });
46
+
47
+ test("export-name matches exported declarations", () => {
48
+ const sf = parse(`export const a = 1; const b = 2; export function foo() {}`);
49
+ const sel = resolveSelector("export-name");
50
+ const nodes = sel(sf);
51
+ expect(nodes).toHaveLength(2);
52
+ });
53
+
54
+ test("import-source matches import declarations", () => {
55
+ const sf = parse(`import { Bucket } from "@aws/s3"; import * as iam from "@aws/iam";`);
56
+ const sel = resolveSelector("import-source");
57
+ const nodes = sel(sf);
58
+ expect(nodes).toHaveLength(2);
59
+ });
60
+
61
+ test("property matches property assignments", () => {
62
+ const sf = parse(`const x = { a: 1, b: 2 };`);
63
+ const sel = resolveSelector("property");
64
+ const nodes = sel(sf);
65
+ expect(nodes).toHaveLength(2);
66
+ });
67
+
68
+ test("resource-type matches new expressions with type arguments", () => {
69
+ const sf = parse(`const b = new Bucket<MyType>({}); const c = new Table({});`);
70
+ const sel = resolveSelector("resource-type");
71
+ const nodes = sel(sf);
72
+ expect(nodes).toHaveLength(1);
73
+ });
74
+
75
+ test("exported-const matches exported const declarations", () => {
76
+ const sf = parse(`export const a = 1; export let b = 2; const c = 3;`);
77
+ const sel = resolveSelector("exported-const");
78
+ const nodes = sel(sf);
79
+ expect(nodes).toHaveLength(1);
80
+ });
81
+ });
82
+
83
+ describe("compound selectors", () => {
84
+ test("resource > property scopes to children", () => {
85
+ const sf = parse(`const b = new Bucket({ name: "test", region: "us" }); const other = { x: 1 };`);
86
+ const sel = resolveSelector("resource > property");
87
+ const nodes = sel(sf);
88
+ expect(nodes).toHaveLength(2);
89
+ });
90
+ });
91
+
92
+ describe("registerSelector", () => {
93
+ test("registers and resolves custom selector", () => {
94
+ registerSelector("number-literal", (sf) =>
95
+ collectNodes(sf, (node) => ts.isNumericLiteral(node))
96
+ );
97
+ const sf = parse(`const a = 42; const b = "hello";`);
98
+ const sel = resolveSelector("number-literal");
99
+ const nodes = sel(sf);
100
+ expect(nodes).toHaveLength(1);
101
+ });
102
+ });
103
+
104
+ describe("error handling", () => {
105
+ test("throws for unknown selector", () => {
106
+ expect(() => resolveSelector("nonexistent")).toThrow('Unknown selector: "nonexistent"');
107
+ });
108
+
109
+ test("throws for unknown compound part", () => {
110
+ expect(() => resolveSelector("resource > nonexistent")).toThrow('Unknown selector: "nonexistent"');
111
+ });
112
+ });
113
+ });