@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,69 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { declarableNamingConventionRule } from "./declarable-naming-convention";
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("COR005: declarable-naming-convention", () => {
23
+ test("rule metadata", () => {
24
+ expect(declarableNamingConventionRule.id).toBe("COR005");
25
+ expect(declarableNamingConventionRule.severity).toBe("warning");
26
+ expect(declarableNamingConventionRule.category).toBe("style");
27
+ });
28
+
29
+ test("triggers on PascalCase declarable name", () => {
30
+ const ctx = createContext(`export const DataBucket = new Bucket({ bucketName: "x" });`);
31
+ const diags = declarableNamingConventionRule.check(ctx);
32
+ expect(diags).toHaveLength(1);
33
+ expect(diags[0].ruleId).toBe("COR005");
34
+ expect(diags[0].message).toContain("dataBucket");
35
+ });
36
+
37
+ test("triggers on UPPER_SNAKE_CASE declarable name", () => {
38
+ const ctx = createContext(`export const DATA_BUCKET = new Bucket({ bucketName: "x" });`);
39
+ const diags = declarableNamingConventionRule.check(ctx);
40
+ expect(diags).toHaveLength(1);
41
+ expect(diags[0].ruleId).toBe("COR005");
42
+ });
43
+
44
+ test("does not trigger on camelCase declarable name", () => {
45
+ const ctx = createContext(`export const dataBucket = new Bucket({ bucketName: "x" });`);
46
+ const diags = declarableNamingConventionRule.check(ctx);
47
+ expect(diags).toHaveLength(0);
48
+ });
49
+
50
+ test("does not trigger on function calls (not new expressions)", () => {
51
+ const ctx = createContext(`export const DataBucket = createBucket({ bucketName: "x" });`);
52
+ const diags = declarableNamingConventionRule.check(ctx);
53
+ expect(diags).toHaveLength(0);
54
+ });
55
+
56
+ test("does not trigger on non-exported declarations", () => {
57
+ const ctx = createContext(`const DataBucket = new Bucket({ bucketName: "x" });`);
58
+ const diags = declarableNamingConventionRule.check(ctx);
59
+ expect(diags).toHaveLength(0);
60
+ });
61
+
62
+ test("reports correct line and column", () => {
63
+ const ctx = createContext(`export const DataBucket = new Bucket({});`);
64
+ const diags = declarableNamingConventionRule.check(ctx);
65
+ expect(diags).toHaveLength(1);
66
+ expect(diags[0].line).toBe(1);
67
+ expect(diags[0].column).toBeGreaterThan(0);
68
+ });
69
+ });
@@ -0,0 +1,70 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR005: declarable-naming-convention
6
+ *
7
+ * Exported declarable instances (i.e. `export const x = new SomeClass(...)`)
8
+ * must use camelCase names. Flags PascalCase and UPPER_SNAKE_CASE names.
9
+ *
10
+ * Triggers on: export const DataBucket = new Bucket({ ... })
11
+ * Triggers on: export const DATA_BUCKET = new Bucket({ ... })
12
+ * OK: export const dataBucket = new Bucket({ ... })
13
+ * OK: export const DataBucket = createBucket({ ... }) (not a new expression)
14
+ * OK: const DataBucket = new Bucket({ ... }) (not exported)
15
+ */
16
+
17
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
18
+ if (ts.isVariableStatement(node)) {
19
+ const hasExport = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
20
+ if (!hasExport) {
21
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
22
+ return;
23
+ }
24
+
25
+ for (const decl of node.declarationList.declarations) {
26
+ if (!ts.isIdentifier(decl.name)) continue;
27
+ if (!decl.initializer || !ts.isNewExpression(decl.initializer)) continue;
28
+
29
+ const name = decl.name.text;
30
+ // camelCase starts with lowercase — that's fine
31
+ if (/^[a-z]/.test(name)) continue;
32
+
33
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
34
+ decl.name.getStart(context.sourceFile),
35
+ );
36
+
37
+ diagnostics.push({
38
+ file: context.filePath,
39
+ line: line + 1,
40
+ column: character + 1,
41
+ ruleId: "COR005",
42
+ severity: "warning",
43
+ message: `Declarable '${name}' should use camelCase naming — rename to '${toCamelCase(name)}'.`,
44
+ });
45
+ }
46
+ }
47
+
48
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
49
+ }
50
+
51
+ /** Suggest a camelCase version of a PascalCase or UPPER_SNAKE_CASE name */
52
+ function toCamelCase(name: string): string {
53
+ // PascalCase -> camelCase: lowercase first char
54
+ if (/^[A-Z][a-z]/.test(name)) {
55
+ return name[0].toLowerCase() + name.slice(1);
56
+ }
57
+ // UPPER_SNAKE -> camelCase
58
+ return name.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
59
+ }
60
+
61
+ export const declarableNamingConventionRule: LintRule = {
62
+ id: "COR005",
63
+ severity: "warning",
64
+ category: "style",
65
+ check(context: LintContext): LintDiagnostic[] {
66
+ const diagnostics: LintDiagnostic[] = [];
67
+ checkNode(context.sourceFile, context, diagnostics);
68
+ return diagnostics;
69
+ },
70
+ };
@@ -0,0 +1,169 @@
1
+ import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { enforceBarrelImportRule } from "./enforce-barrel-import";
4
+ import type { LintContext } from "../rule";
5
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
6
+ import { join } from "path";
7
+
8
+ const TEST_DIR = join(import.meta.dir, "__test_barrel_import__");
9
+
10
+ function createContext(code: string, filePath = "test.ts"): LintContext {
11
+ const sourceFile = ts.createSourceFile(
12
+ filePath,
13
+ code,
14
+ ts.ScriptTarget.Latest,
15
+ true
16
+ );
17
+
18
+ return {
19
+ sourceFile,
20
+ entities: [],
21
+ filePath,
22
+ lexicon: undefined,
23
+ };
24
+ }
25
+
26
+ describe("COR014: enforce-barrel-import", () => {
27
+ beforeEach(() => {
28
+ mkdirSync(TEST_DIR, { recursive: true });
29
+ });
30
+
31
+ afterEach(() => {
32
+ rmSync(TEST_DIR, { recursive: true, force: true });
33
+ });
34
+
35
+ test("rule metadata", () => {
36
+ expect(enforceBarrelImportRule.id).toBe("COR014");
37
+ expect(enforceBarrelImportRule.severity).toBe("warning");
38
+ expect(enforceBarrelImportRule.category).toBe("style");
39
+ });
40
+
41
+ test("flags lexicon import in non-barrel file", () => {
42
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
43
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
44
+ const diagnostics = enforceBarrelImportRule.check(context);
45
+
46
+ expect(diagnostics).toHaveLength(1);
47
+ expect(diagnostics[0].ruleId).toBe("COR014");
48
+ expect(diagnostics[0].severity).toBe("warning");
49
+ });
50
+
51
+ test("skips barrel file named _.ts", () => {
52
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
53
+ const context = createContext(code, join(TEST_DIR, "_.ts"));
54
+ const diagnostics = enforceBarrelImportRule.check(context);
55
+ expect(diagnostics).toHaveLength(0);
56
+ });
57
+
58
+ test("skips barrel file with path prefix", () => {
59
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
60
+ const context = createContext(code, join(TEST_DIR, "infra", "_.ts"));
61
+ const diagnostics = enforceBarrelImportRule.check(context);
62
+ expect(diagnostics).toHaveLength(0);
63
+ });
64
+
65
+ test("allows barrel import", () => {
66
+ const code = `import * as _ from "./_";`;
67
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
68
+ const diagnostics = enforceBarrelImportRule.check(context);
69
+ expect(diagnostics).toHaveLength(0);
70
+ });
71
+
72
+ test("allows non-chant imports", () => {
73
+ const code = `import * as ts from "typescript";`;
74
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
75
+ const diagnostics = enforceBarrelImportRule.check(context);
76
+ expect(diagnostics).toHaveLength(0);
77
+ });
78
+
79
+ test("message includes barrel content when barrel missing", () => {
80
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
81
+ const filePath = join(TEST_DIR, "my-stack.ts");
82
+ const context = createContext(code, filePath);
83
+ const diagnostics = enforceBarrelImportRule.check(context);
84
+
85
+ expect(diagnostics).toHaveLength(1);
86
+ expect(diagnostics[0].message).toContain("Create _.ts");
87
+ expect(diagnostics[0].message).toContain(`export * from "@intentius/chant-lexicon-testdom"`);
88
+ expect(diagnostics[0].message).toContain("barrel");
89
+ });
90
+
91
+ test("message is shorter when barrel exists", () => {
92
+ // Create a barrel file in TEST_DIR
93
+ writeFileSync(join(TEST_DIR, "_.ts"), `export * from "@intentius/chant-lexicon-testdom";`);
94
+
95
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
96
+ const filePath = join(TEST_DIR, "my-stack.ts");
97
+ const context = createContext(code, filePath);
98
+ const diagnostics = enforceBarrelImportRule.check(context);
99
+
100
+ expect(diagnostics).toHaveLength(1);
101
+ expect(diagnostics[0].message).not.toContain("Create _.ts");
102
+ expect(diagnostics[0].message).toContain("use the barrel");
103
+ });
104
+
105
+ test("provides auto-fix when barrel exists", () => {
106
+ writeFileSync(join(TEST_DIR, "_.ts"), `export * from "@intentius/chant-lexicon-testdom";`);
107
+
108
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
109
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
110
+ const diagnostics = enforceBarrelImportRule.check(context);
111
+
112
+ expect(diagnostics).toHaveLength(1);
113
+ expect(diagnostics[0].fix).toBeDefined();
114
+ expect(diagnostics[0].fix!.replacement).toBe(`import * as _ from "./_"`);
115
+ });
116
+
117
+ test("does not provide auto-fix when barrel is missing", () => {
118
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
119
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
120
+ const diagnostics = enforceBarrelImportRule.check(context);
121
+
122
+ expect(diagnostics).toHaveLength(1);
123
+ expect(diagnostics[0].fix).toBeUndefined();
124
+ });
125
+
126
+ test("flags type-only lexicon import", () => {
127
+ const code = `import type { Code } from "@intentius/chant-lexicon-testdom";`;
128
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
129
+ const diagnostics = enforceBarrelImportRule.check(context);
130
+
131
+ expect(diagnostics).toHaveLength(1);
132
+ expect(diagnostics[0].ruleId).toBe("COR014");
133
+ });
134
+
135
+ test("skips subpath imports from core", () => {
136
+ const code = `import type { LintRule } from "@intentius/chant/lint/rule";`;
137
+ const context = createContext(code, join(TEST_DIR, "my-rule.ts"));
138
+ const diagnostics = enforceBarrelImportRule.check(context);
139
+ expect(diagnostics).toHaveLength(0);
140
+ });
141
+
142
+ test("skips deep subpath imports from lexicon", () => {
143
+ const code = `import { helper } from "@intentius/chant-lexicon-aws/internal/util";`;
144
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
145
+ const diagnostics = enforceBarrelImportRule.check(context);
146
+ expect(diagnostics).toHaveLength(0);
147
+ });
148
+
149
+ test("flags multiple lexicon imports", () => {
150
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";
151
+ import * as core from "@intentius/chant";`;
152
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
153
+ const diagnostics = enforceBarrelImportRule.check(context);
154
+
155
+ expect(diagnostics).toHaveLength(2);
156
+ expect(diagnostics[0].message).toContain("@intentius/chant-lexicon-testdom");
157
+ expect(diagnostics[1].message).toContain("@intentius/chant");
158
+ });
159
+
160
+ test("reports correct line and column numbers", () => {
161
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
162
+ const context = createContext(code, join(TEST_DIR, "my-stack.ts"));
163
+ const diagnostics = enforceBarrelImportRule.check(context);
164
+
165
+ expect(diagnostics).toHaveLength(1);
166
+ expect(diagnostics[0].line).toBe(1);
167
+ expect(diagnostics[0].column).toBe(1);
168
+ });
169
+ });
@@ -0,0 +1,81 @@
1
+ import * as ts from "typescript";
2
+ import { existsSync } from "fs";
3
+ import { dirname, join, basename } from "path";
4
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
5
+
6
+ /**
7
+ * COR014: enforce-barrel-import
8
+ *
9
+ * Flags direct lexicon imports (`import * as <name> from "@intentius/chant-lexicon-<name>"`)
10
+ * in non-barrel files. Use the barrel (`import * as _ from "./_"`) instead.
11
+ *
12
+ * Triggers on: import * as <name> from "@intentius/chant-lexicon-<name>" (in non-barrel files)
13
+ * OK: import * as _ from "./_"
14
+ * OK: import * as <name> from "@intentius/chant-lexicon-<name>" (in _.ts barrel files)
15
+ */
16
+
17
+ export const enforceBarrelImportRule: LintRule = {
18
+ id: "COR014",
19
+ severity: "warning",
20
+ category: "style",
21
+ check(context: LintContext): LintDiagnostic[] {
22
+ const diagnostics: LintDiagnostic[] = [];
23
+ const sf = context.sourceFile;
24
+
25
+ // Skip barrel files
26
+ if (basename(context.filePath).startsWith("_")) return diagnostics;
27
+
28
+ // Check if barrel exists
29
+ const dir = dirname(context.filePath);
30
+ const barrelExists = existsSync(join(dir, "_.ts"));
31
+
32
+ for (const stmt of sf.statements) {
33
+ if (!ts.isImportDeclaration(stmt)) continue;
34
+ if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
35
+
36
+ const modulePath = stmt.moduleSpecifier.text;
37
+ if (!modulePath.startsWith("@intentius/chant") && !modulePath.startsWith("@intentius/chant-lexicon-")) continue;
38
+
39
+ // Skip subpath imports (e.g., @intentius/chant/lint/rule) — these are
40
+ // framework internals not available through the barrel
41
+ const parts = modulePath.split("/");
42
+ if (parts.length > 2) continue;
43
+
44
+ const { line, character } = sf.getLineAndCharacterOfPosition(stmt.getStart(sf));
45
+ const importText = stmt.getText(sf);
46
+
47
+ let message: string;
48
+ if (barrelExists) {
49
+ message = `Direct lexicon import — use the barrel.\n - ${importText}\n + import * as _ from "./_";`;
50
+ } else {
51
+ const lexiconPkg = modulePath;
52
+ message =
53
+ `Direct lexicon import — use a barrel file.\n\n` +
54
+ ` Create _.ts:\n\n` +
55
+ ` export * from "${lexiconPkg}";\n` +
56
+ ` import { barrel } from "@intentius/chant";\n` +
57
+ ` export const $ = barrel(import.meta.dir);\n\n` +
58
+ ` Then replace this file's import:\n` +
59
+ ` - ${importText}\n` +
60
+ ` + import * as _ from "./_";`;
61
+ }
62
+
63
+ diagnostics.push({
64
+ file: context.filePath,
65
+ line: line + 1,
66
+ column: character + 1,
67
+ ruleId: "COR014",
68
+ severity: "warning",
69
+ message,
70
+ // Only provide auto-fix when barrel exists — replacing the import
71
+ // when no barrel is present corrupts the file
72
+ fix: barrelExists ? {
73
+ range: [stmt.getStart(sf), stmt.getEnd()],
74
+ replacement: `import * as _ from "./_"`,
75
+ } : undefined,
76
+ });
77
+ }
78
+
79
+ return diagnostics;
80
+ },
81
+ };
@@ -0,0 +1,114 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { enforceBarrelRefRule } from "./enforce-barrel-ref";
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("COR007: enforce-barrel-ref", () => {
23
+ test("rule metadata", () => {
24
+ expect(enforceBarrelRefRule.id).toBe("COR007");
25
+ expect(enforceBarrelRefRule.severity).toBe("warning");
26
+ expect(enforceBarrelRefRule.category).toBe("style");
27
+ });
28
+
29
+ test("flags named import from sibling", () => {
30
+ const code = `import { dataBucket } from "./data-bucket";`;
31
+ const context = createContext(code);
32
+ const diagnostics = enforceBarrelRefRule.check(context);
33
+
34
+ expect(diagnostics).toHaveLength(1);
35
+ expect(diagnostics[0].ruleId).toBe("COR007");
36
+ expect(diagnostics[0].severity).toBe("warning");
37
+ });
38
+
39
+ test("flags namespace import from sibling", () => {
40
+ const code = `import * as db from "./data-bucket";`;
41
+ const context = createContext(code);
42
+ const diagnostics = enforceBarrelRefRule.check(context);
43
+
44
+ expect(diagnostics).toHaveLength(1);
45
+ expect(diagnostics[0].ruleId).toBe("COR007");
46
+ });
47
+
48
+ test("skips barrel import", () => {
49
+ const code = `import * as _ from "./_";`;
50
+ const context = createContext(code);
51
+ const diagnostics = enforceBarrelRefRule.check(context);
52
+ expect(diagnostics).toHaveLength(0);
53
+ });
54
+
55
+ test("skips lexicon imports", () => {
56
+ const code = `import * as td from "@intentius/chant-lexicon-testdom";`;
57
+ const context = createContext(code);
58
+ const diagnostics = enforceBarrelRefRule.check(context);
59
+ expect(diagnostics).toHaveLength(0);
60
+ });
61
+
62
+ test("skips barrel file", () => {
63
+ const code = `import { dataBucket } from "./data-bucket";`;
64
+ const context = createContext(code, "_.ts");
65
+ const diagnostics = enforceBarrelRefRule.check(context);
66
+ expect(diagnostics).toHaveLength(0);
67
+ });
68
+
69
+ test("message includes suggestion with _.$ prefix", () => {
70
+ const code = `import { dataBucket } from "./data-bucket";`;
71
+ const context = createContext(code);
72
+ const diagnostics = enforceBarrelRefRule.check(context);
73
+
74
+ expect(diagnostics).toHaveLength(1);
75
+ expect(diagnostics[0].message).toContain("_.$.dataBucket");
76
+ });
77
+
78
+ test("flags multiple sibling imports", () => {
79
+ const code = `import { dataBucket } from "./data-bucket";
80
+ import { logGroup } from "./log-group";`;
81
+ const context = createContext(code);
82
+ const diagnostics = enforceBarrelRefRule.check(context);
83
+
84
+ expect(diagnostics).toHaveLength(2);
85
+ });
86
+
87
+ test("flags parent relative imports", () => {
88
+ const code = `import { x } from "../other";`;
89
+ const context = createContext(code);
90
+ const diagnostics = enforceBarrelRefRule.check(context);
91
+
92
+ expect(diagnostics).toHaveLength(1);
93
+ expect(diagnostics[0].ruleId).toBe("COR007");
94
+ });
95
+
96
+ test("does not provide auto-fix", () => {
97
+ const code = `import { dataBucket } from "./data-bucket";`;
98
+ const context = createContext(code);
99
+ const diagnostics = enforceBarrelRefRule.check(context);
100
+
101
+ expect(diagnostics).toHaveLength(1);
102
+ expect(diagnostics[0].fix).toBeUndefined();
103
+ });
104
+
105
+ test("reports correct line and column numbers", () => {
106
+ const code = `import { dataBucket } from "./data-bucket";`;
107
+ const context = createContext(code);
108
+ const diagnostics = enforceBarrelRefRule.check(context);
109
+
110
+ expect(diagnostics).toHaveLength(1);
111
+ expect(diagnostics[0].line).toBe(1);
112
+ expect(diagnostics[0].column).toBe(1);
113
+ });
114
+ });
@@ -0,0 +1,75 @@
1
+ import * as ts from "typescript";
2
+ import { basename } from "path";
3
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
4
+
5
+ /**
6
+ * COR007: enforce-barrel-ref
7
+ *
8
+ * Flags direct sibling imports (`import { dataBucket } from "./data-bucket"`)
9
+ * in non-barrel files. Use `_.$` instead.
10
+ *
11
+ * Triggers on: import { dataBucket } from "./data-bucket"
12
+ * OK: import * as _ from "./_"
13
+ * OK: import { dataBucket } from "./data-bucket" (in _.ts barrel files)
14
+ */
15
+
16
+ const barrelPattern = /^\.\/(_|_\..*)$/;
17
+
18
+ export const enforceBarrelRefRule: LintRule = {
19
+ id: "COR007",
20
+ severity: "warning",
21
+ category: "style",
22
+ check(context: LintContext): LintDiagnostic[] {
23
+ const diagnostics: LintDiagnostic[] = [];
24
+ const sf = context.sourceFile;
25
+
26
+ // Skip barrel files
27
+ if (basename(context.filePath).startsWith("_")) return diagnostics;
28
+
29
+ for (const stmt of sf.statements) {
30
+ if (!ts.isImportDeclaration(stmt)) continue;
31
+ if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;
32
+
33
+ const modulePath = stmt.moduleSpecifier.text;
34
+
35
+ // Skip non-relative imports
36
+ if (!modulePath.startsWith("./") && !modulePath.startsWith("../")) continue;
37
+
38
+ // Skip barrel imports
39
+ if (barrelPattern.test(modulePath)) continue;
40
+
41
+ // This is a direct sibling import — flag it
42
+ const { line, character } = sf.getLineAndCharacterOfPosition(stmt.getStart(sf));
43
+ const importText = stmt.getText(sf);
44
+
45
+ // Extract imported names for the suggestion
46
+ const importedNames: string[] = [];
47
+ const clause = stmt.importClause;
48
+ if (clause?.namedBindings) {
49
+ if (ts.isNamedImports(clause.namedBindings)) {
50
+ for (const el of clause.namedBindings.elements) {
51
+ importedNames.push(el.name.text);
52
+ }
53
+ } else if (ts.isNamespaceImport(clause.namedBindings)) {
54
+ importedNames.push(clause.namedBindings.name.text + ".*");
55
+ }
56
+ }
57
+
58
+ const refSuggestion =
59
+ importedNames.length > 0
60
+ ? `\n Access via: ${importedNames.map((n) => `_.$.${n}`).join(", ")}`
61
+ : "";
62
+
63
+ diagnostics.push({
64
+ file: context.filePath,
65
+ line: line + 1,
66
+ column: character + 1,
67
+ ruleId: "COR007",
68
+ severity: "warning",
69
+ message: `Direct sibling import — use _.$ instead.\n - ${importText}${refSuggestion}`,
70
+ });
71
+ }
72
+
73
+ return diagnostics;
74
+ },
75
+ };