@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,158 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR008: Export required
6
+ *
7
+ * Detects declarable instances that are not exported.
8
+ * All declarable instances should be exported to be tracked by chant.
9
+ *
10
+ * Triggers on: new Bucket({...}) without export
11
+ * OK: export const bucket = new Bucket({...})
12
+ */
13
+
14
+ interface ExportInfo {
15
+ exportedNames: Set<string>;
16
+ localVariableNames: Set<string>;
17
+ }
18
+
19
+ function isDeclarableClass(node: ts.Node, sourceFile: ts.SourceFile): boolean {
20
+ // Check if it's a class with Declarable interface implementation
21
+ if (ts.isClassDeclaration(node) && node.heritageClauses) {
22
+ for (const clause of node.heritageClauses) {
23
+ for (const type of clause.types) {
24
+ const typeName = type.expression.getText(sourceFile);
25
+ if (typeName === "Declarable") {
26
+ return true;
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ function collectExportInfo(sourceFile: ts.SourceFile): ExportInfo {
35
+ const exportedNames = new Set<string>();
36
+ const localVariableNames = new Set<string>();
37
+
38
+ function visit(node: ts.Node): void {
39
+ // Handle exported variable declarations
40
+ if (ts.isVariableStatement(node)) {
41
+ const hasExportModifier = node.modifiers?.some(
42
+ m => m.kind === ts.SyntaxKind.ExportKeyword
43
+ );
44
+
45
+ for (const declaration of node.declarationList.declarations) {
46
+ if (ts.isIdentifier(declaration.name)) {
47
+ const name = declaration.name.text;
48
+ if (hasExportModifier) {
49
+ exportedNames.add(name);
50
+ } else {
51
+ localVariableNames.add(name);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // Handle export assignments: export = something
58
+ if (ts.isExportAssignment(node)) {
59
+ if (ts.isIdentifier(node.expression)) {
60
+ exportedNames.add(node.expression.text);
61
+ }
62
+ }
63
+
64
+ ts.forEachChild(node, visit);
65
+ }
66
+
67
+ visit(sourceFile);
68
+ return { exportedNames, localVariableNames };
69
+ }
70
+
71
+ function isDeclarableNewExpression(node: ts.NewExpression, context: LintContext): boolean {
72
+ // Check if the class being instantiated implements Declarable
73
+ // We look for the class declaration in the source file or imports
74
+ const className = node.expression.getText(context.sourceFile);
75
+
76
+ // Check local class declarations
77
+ let isDeclarable = false;
78
+
79
+ function checkClassDeclarations(n: ts.Node): void {
80
+ if (ts.isClassDeclaration(n) && n.name?.text === className) {
81
+ if (isDeclarableClass(n, context.sourceFile)) {
82
+ isDeclarable = true;
83
+ }
84
+ }
85
+ ts.forEachChild(n, checkClassDeclarations);
86
+ }
87
+
88
+ checkClassDeclarations(context.sourceFile);
89
+
90
+ // For imported classes, we use a heuristic: check if the class name is capitalized
91
+ // and follows common declarable patterns (like Bucket, Parameter, etc.)
92
+ if (!isDeclarable && /^[A-Z]/.test(className)) {
93
+ // Additional check: see if it's imported from a declarable-looking module
94
+ // For now, we'll be conservative and only flag classes that are clearly local
95
+ return isDeclarable;
96
+ }
97
+
98
+ return isDeclarable;
99
+ }
100
+
101
+ function checkNode(
102
+ node: ts.Node,
103
+ context: LintContext,
104
+ exportInfo: ExportInfo,
105
+ diagnostics: LintDiagnostic[]
106
+ ): void {
107
+ // Check for new expressions that are not exported
108
+ if (ts.isNewExpression(node)) {
109
+ // Skip if this is part of a variable initializer that's exported
110
+ let parent = node.parent;
111
+ let isPartOfExportedVariable = false;
112
+ let variableName: string | undefined;
113
+
114
+ while (parent) {
115
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
116
+ variableName = parent.name.text;
117
+ if (exportInfo.exportedNames.has(variableName)) {
118
+ isPartOfExportedVariable = true;
119
+ break;
120
+ }
121
+ }
122
+ parent = parent.parent;
123
+ }
124
+
125
+ if (!isPartOfExportedVariable && isDeclarableNewExpression(node, context)) {
126
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
127
+ node.getStart(context.sourceFile)
128
+ );
129
+
130
+ const className = node.expression.getText(context.sourceFile);
131
+ const label = variableName ? `'${variableName}' (${className})` : `'${className}'`;
132
+
133
+ diagnostics.push({
134
+ file: context.filePath,
135
+ line: line + 1,
136
+ column: character + 1,
137
+ ruleId: "COR008",
138
+ severity: "warning",
139
+ message: `Declarable ${label} is not exported — add 'export' so chant can discover it during synthesis.`,
140
+ });
141
+ }
142
+ }
143
+
144
+ // Recursively check child nodes
145
+ ts.forEachChild(node, child => checkNode(child, context, exportInfo, diagnostics));
146
+ }
147
+
148
+ export const exportRequiredRule: LintRule = {
149
+ id: "COR008",
150
+ severity: "warning",
151
+ category: "correctness",
152
+ check(context: LintContext): LintDiagnostic[] {
153
+ const diagnostics: LintDiagnostic[] = [];
154
+ const exportInfo = collectExportInfo(context.sourceFile);
155
+ checkNode(context.sourceFile, context, exportInfo, diagnostics);
156
+ return diagnostics;
157
+ },
158
+ };
@@ -0,0 +1,148 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { fileDeclarableLimitRule } from "./file-declarable-limit";
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
+ function makeNewExprs(names: string[]): string {
23
+ return names.map((n) => `const x = new ${n}({ name: "a" });`).join("\n");
24
+ }
25
+
26
+ describe("COR009: file-declarable-limit", () => {
27
+ test("rule metadata", () => {
28
+ expect(fileDeclarableLimitRule.id).toBe("COR009");
29
+ expect(fileDeclarableLimitRule.severity).toBe("warning");
30
+ expect(fileDeclarableLimitRule.category).toBe("style");
31
+ });
32
+
33
+ test("does not warn with 3 Declarable instances", () => {
34
+ const code = makeNewExprs(["Bucket", "Table", "Queue"]);
35
+ const context = createContext(code);
36
+ const diagnostics = fileDeclarableLimitRule.check(context);
37
+ expect(diagnostics).toHaveLength(0);
38
+ });
39
+
40
+ test("does not warn with exactly 8 Declarable instances", () => {
41
+ const code = makeNewExprs([
42
+ "Bucket",
43
+ "Table",
44
+ "Queue",
45
+ "Topic",
46
+ "Function",
47
+ "Role",
48
+ "Policy",
49
+ "Stack",
50
+ ]);
51
+ const context = createContext(code);
52
+ const diagnostics = fileDeclarableLimitRule.check(context);
53
+ expect(diagnostics).toHaveLength(0);
54
+ });
55
+
56
+ test("warns with 9 Declarable instances", () => {
57
+ const code = makeNewExprs([
58
+ "Bucket",
59
+ "Table",
60
+ "Queue",
61
+ "Topic",
62
+ "Function",
63
+ "Role",
64
+ "Policy",
65
+ "Stack",
66
+ "Alarm",
67
+ ]);
68
+ const context = createContext(code);
69
+ const diagnostics = fileDeclarableLimitRule.check(context);
70
+
71
+ expect(diagnostics).toHaveLength(1);
72
+ expect(diagnostics[0].ruleId).toBe("COR009");
73
+ expect(diagnostics[0].severity).toBe("warning");
74
+ expect(diagnostics[0].line).toBe(1);
75
+ expect(diagnostics[0].column).toBe(1);
76
+ expect(diagnostics[0].message).toBe(
77
+ "File contains 9 Declarable instances (limit: 8) — consider splitting into separate files by concern",
78
+ );
79
+ });
80
+
81
+ test("does not count non-Declarable new expressions", () => {
82
+ const code = [
83
+ `const d = new Date();`,
84
+ `const m = new Map();`,
85
+ `const s = new Set();`,
86
+ `const e = new Error("oops");`,
87
+ `const r = new RegExp("abc");`,
88
+ `const a = new Array(10);`,
89
+ `const w = new WeakMap();`,
90
+ `const p = new Promise(() => {});`,
91
+ `const u = new URL("https://example.com");`,
92
+ ].join("\n");
93
+ const context = createContext(code);
94
+ const diagnostics = fileDeclarableLimitRule.check(context);
95
+ expect(diagnostics).toHaveLength(0);
96
+ });
97
+
98
+ test("only counts capitalized constructors as Declarable", () => {
99
+ // Mix of lowercase (non-Declarable) and uppercase (Declarable)
100
+ const code = [
101
+ ...Array.from({ length: 10 }, () => `new foo();`),
102
+ `new Bucket({ name: "a" });`,
103
+ ].join("\n");
104
+ const context = createContext(code);
105
+ const diagnostics = fileDeclarableLimitRule.check(context);
106
+ expect(diagnostics).toHaveLength(0);
107
+ });
108
+
109
+ test("reports correct file path", () => {
110
+ const code = makeNewExprs([
111
+ "A",
112
+ "B",
113
+ "C",
114
+ "D",
115
+ "E",
116
+ "F",
117
+ "G",
118
+ "H",
119
+ "I",
120
+ ]);
121
+ const context = createContext(code, "infra/my-stack.ts");
122
+ const diagnostics = fileDeclarableLimitRule.check(context);
123
+
124
+ expect(diagnostics).toHaveLength(1);
125
+ expect(diagnostics[0].file).toBe("infra/my-stack.ts");
126
+ });
127
+
128
+ test("message includes actual count", () => {
129
+ const code = makeNewExprs([
130
+ "A",
131
+ "B",
132
+ "C",
133
+ "D",
134
+ "E",
135
+ "F",
136
+ "G",
137
+ "H",
138
+ "I",
139
+ "J",
140
+ "K",
141
+ ]);
142
+ const context = createContext(code);
143
+ const diagnostics = fileDeclarableLimitRule.check(context);
144
+
145
+ expect(diagnostics).toHaveLength(1);
146
+ expect(diagnostics[0].message).toContain("11 Declarable instances");
147
+ });
148
+ });
@@ -0,0 +1,96 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ const DECLARABLE_LIMIT = 8;
5
+
6
+ const BUILTIN_CONSTRUCTORS = new Set([
7
+ "Array",
8
+ "ArrayBuffer",
9
+ "BigInt64Array",
10
+ "BigUint64Array",
11
+ "Boolean",
12
+ "DataView",
13
+ "Date",
14
+ "Error",
15
+ "EvalError",
16
+ "Float32Array",
17
+ "Float64Array",
18
+ "Int8Array",
19
+ "Int16Array",
20
+ "Int32Array",
21
+ "Map",
22
+ "Number",
23
+ "Object",
24
+ "Promise",
25
+ "Proxy",
26
+ "RangeError",
27
+ "ReferenceError",
28
+ "RegExp",
29
+ "Set",
30
+ "SharedArrayBuffer",
31
+ "String",
32
+ "SyntaxError",
33
+ "TypeError",
34
+ "URIError",
35
+ "URL",
36
+ "URLSearchParams",
37
+ "Uint8Array",
38
+ "Uint8ClampedArray",
39
+ "Uint16Array",
40
+ "Uint32Array",
41
+ "WeakMap",
42
+ "WeakRef",
43
+ "WeakSet",
44
+ ]);
45
+
46
+ function isDeclarableConstructor(node: ts.NewExpression): boolean {
47
+ const expr = node.expression;
48
+ if (ts.isIdentifier(expr)) {
49
+ const name = expr.text;
50
+ return (
51
+ name.length > 0 &&
52
+ name[0] >= "A" &&
53
+ name[0] <= "Z" &&
54
+ !BUILTIN_CONSTRUCTORS.has(name)
55
+ );
56
+ }
57
+ return false;
58
+ }
59
+
60
+ function collectDeclarableNewExpressions(
61
+ node: ts.Node,
62
+ results: ts.NewExpression[],
63
+ ): void {
64
+ if (ts.isNewExpression(node) && isDeclarableConstructor(node)) {
65
+ results.push(node);
66
+ }
67
+ ts.forEachChild(node, (child) =>
68
+ collectDeclarableNewExpressions(child, results),
69
+ );
70
+ }
71
+
72
+ export const fileDeclarableLimitRule: LintRule = {
73
+ id: "COR009",
74
+ severity: "warning",
75
+ category: "style",
76
+ check(context: LintContext, options?: Record<string, unknown>): LintDiagnostic[] {
77
+ const limit = (typeof options?.max === "number" ? options.max : null) ?? DECLARABLE_LIMIT;
78
+ const instances: ts.NewExpression[] = [];
79
+ collectDeclarableNewExpressions(context.sourceFile, instances);
80
+
81
+ if (instances.length > limit) {
82
+ return [
83
+ {
84
+ file: context.filePath,
85
+ line: 1,
86
+ column: 1,
87
+ ruleId: "COR009",
88
+ severity: "warning",
89
+ message: `File contains ${instances.length} Declarable instances (limit: ${limit}) — consider splitting into separate files by concern`,
90
+ },
91
+ ];
92
+ }
93
+
94
+ return [];
95
+ },
96
+ };
@@ -0,0 +1,210 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { flatDeclarationsRule } from "./flat-declarations";
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("COR001: flat-declarations", () => {
23
+ test("rule metadata", () => {
24
+ expect(flatDeclarationsRule.id).toBe("COR001");
25
+ expect(flatDeclarationsRule.severity).toBe("warning");
26
+ expect(flatDeclarationsRule.category).toBe("style");
27
+ });
28
+
29
+ test("triggers on inline object literal in constructor", () => {
30
+ const code = `new Bucket({ bucketEncryption: { serverSideEncryptionConfiguration: [] } });`;
31
+ const context = createContext(code);
32
+ const diagnostics = flatDeclarationsRule.check(context);
33
+
34
+ expect(diagnostics).toHaveLength(1);
35
+ expect(diagnostics[0].ruleId).toBe("COR001");
36
+ expect(diagnostics[0].severity).toBe("warning");
37
+ expect(diagnostics[0].message).toBe("Inline object in Declarable constructor — extract to a named 'const' with 'export'. Each config value should be its own Declarable.");
38
+ });
39
+
40
+ test("triggers on inline array literal in constructor", () => {
41
+ const code = `new Bucket({ tags: [{ key: "env", value: "prod" }] });`;
42
+ const context = createContext(code);
43
+ const diagnostics = flatDeclarationsRule.check(context);
44
+
45
+ expect(diagnostics).toHaveLength(1);
46
+ expect(diagnostics[0].ruleId).toBe("COR001");
47
+ expect(diagnostics[0].message).toBe("Inline object in Declarable constructor — extract to a named 'const' with 'export'. Each config value should be its own Declarable.");
48
+ });
49
+
50
+ test("does not trigger on primitive values in constructor", () => {
51
+ const code = `new Bucket({ bucketName: "my-bucket", accessControl: "Private" });`;
52
+ const context = createContext(code);
53
+ const diagnostics = flatDeclarationsRule.check(context);
54
+ expect(diagnostics).toHaveLength(0);
55
+ });
56
+
57
+ test("does not trigger on identifier references in constructor", () => {
58
+ const code = `new Bucket({ encryption: dataEncryption, publicAccessBlock: dataAccessBlock });`;
59
+ const context = createContext(code);
60
+ const diagnostics = flatDeclarationsRule.check(context);
61
+ expect(diagnostics).toHaveLength(0);
62
+ });
63
+
64
+ test("does not trigger on intrinsic calls in constructor", () => {
65
+ const code = `new Bucket({ bucketName: Sub\`\${AWS.StackName}-data\` });`;
66
+ const context = createContext(code);
67
+ const diagnostics = flatDeclarationsRule.check(context);
68
+ expect(diagnostics).toHaveLength(0);
69
+ });
70
+
71
+ test("does not trigger on function/method calls in constructor", () => {
72
+ const code = `new Bucket({ encryption: getEncryption() });`;
73
+ const context = createContext(code);
74
+ const diagnostics = flatDeclarationsRule.check(context);
75
+ expect(diagnostics).toHaveLength(0);
76
+ });
77
+
78
+ test("triggers on multiple inline objects in same constructor", () => {
79
+ const code = `new Bucket({
80
+ encryption: { type: "AES256" },
81
+ tags: [{ key: "env" }]
82
+ });`;
83
+ const context = createContext(code);
84
+ const diagnostics = flatDeclarationsRule.check(context);
85
+
86
+ expect(diagnostics).toHaveLength(2);
87
+ expect(diagnostics[0].ruleId).toBe("COR001");
88
+ expect(diagnostics[1].ruleId).toBe("COR001");
89
+ });
90
+
91
+ test("triggers on multiple constructor calls with violations", () => {
92
+ const code = `
93
+ new Bucket({ encryption: { type: "AES256" } });
94
+ new Table({ schema: { fields: [] } });
95
+ `;
96
+ const context = createContext(code);
97
+ const diagnostics = flatDeclarationsRule.check(context);
98
+
99
+ expect(diagnostics).toHaveLength(2);
100
+ expect(diagnostics[0].ruleId).toBe("COR001");
101
+ expect(diagnostics[1].ruleId).toBe("COR001");
102
+ });
103
+
104
+ test("does not trigger on empty object in constructor", () => {
105
+ const code = `new Bucket({});`;
106
+ const context = createContext(code);
107
+ const diagnostics = flatDeclarationsRule.check(context);
108
+ expect(diagnostics).toHaveLength(0);
109
+ });
110
+
111
+ test("does not trigger on constructor with no arguments", () => {
112
+ const code = `new Bucket();`;
113
+ const context = createContext(code);
114
+ const diagnostics = flatDeclarationsRule.check(context);
115
+ expect(diagnostics).toHaveLength(0);
116
+ });
117
+
118
+ test("does not trigger on constructor with non-object argument", () => {
119
+ const code = `new SomeClass("string-arg");`;
120
+ const context = createContext(code);
121
+ const diagnostics = flatDeclarationsRule.check(context);
122
+ expect(diagnostics).toHaveLength(0);
123
+ });
124
+
125
+ test("reports correct line and column numbers", () => {
126
+ const code = `new Bucket({ encryption: { type: "AES256" } });`;
127
+ const context = createContext(code);
128
+ const diagnostics = flatDeclarationsRule.check(context);
129
+
130
+ expect(diagnostics).toHaveLength(1);
131
+ expect(diagnostics[0].line).toBe(1);
132
+ expect(diagnostics[0].column).toBeGreaterThan(0);
133
+ expect(diagnostics[0].file).toBe("test.ts");
134
+ });
135
+
136
+ test("handles mixed primitive and object properties in constructor", () => {
137
+ const code = `new Bucket({
138
+ bucketName: "my-bucket",
139
+ encryption: { type: "AES256" },
140
+ accessControl: "Private"
141
+ });`;
142
+ const context = createContext(code);
143
+ const diagnostics = flatDeclarationsRule.check(context);
144
+
145
+ expect(diagnostics).toHaveLength(1);
146
+ expect(diagnostics[0].message).toBe("Inline object in Declarable constructor — extract to a named 'const' with 'export'. Each config value should be its own Declarable.");
147
+ });
148
+
149
+ test("does not trigger on non-constructor object literals", () => {
150
+ const code = `export const x = { a: { b: 1 } };`;
151
+ const context = createContext(code);
152
+ const diagnostics = flatDeclarationsRule.check(context);
153
+ expect(diagnostics).toHaveLength(0);
154
+ });
155
+
156
+ test("handles nested constructors", () => {
157
+ const code = `new Outer({ inner: new Inner({ config: { value: 1 } }) });`;
158
+ const context = createContext(code);
159
+ const diagnostics = flatDeclarationsRule.check(context);
160
+
161
+ // Should flag the inline object in the Inner constructor
162
+ expect(diagnostics).toHaveLength(1);
163
+ expect(diagnostics[0].ruleId).toBe("COR001");
164
+ });
165
+
166
+ test("does not trigger inside Composite() factory callback", () => {
167
+ const code = `
168
+ const MyComposite = Composite((props) => {
169
+ const role = new Role({
170
+ assumeRolePolicyDocument: {
171
+ Version: "2012-10-17",
172
+ Statement: [{ Effect: "Allow" }],
173
+ },
174
+ });
175
+ return { role };
176
+ });
177
+ `;
178
+ const context = createContext(code);
179
+ const diagnostics = flatDeclarationsRule.check(context);
180
+ expect(diagnostics).toHaveLength(0);
181
+ });
182
+
183
+ test("does not trigger inside _.Composite() factory callback", () => {
184
+ const code = `
185
+ const MyComposite = _.Composite((props) => {
186
+ const role = new _.Role({
187
+ policies: [{ policyName: "CustomPolicy" }],
188
+ });
189
+ return { role };
190
+ });
191
+ `;
192
+ const context = createContext(code);
193
+ const diagnostics = flatDeclarationsRule.check(context);
194
+ expect(diagnostics).toHaveLength(0);
195
+ });
196
+
197
+ test("still triggers outside Composite() in same file", () => {
198
+ const code = `
199
+ const MyComposite = Composite((props) => {
200
+ const role = new Role({ config: { value: 1 } });
201
+ return { role };
202
+ });
203
+ const bucket = new Bucket({ encryption: { type: "AES256" } });
204
+ `;
205
+ const context = createContext(code);
206
+ const diagnostics = flatDeclarationsRule.check(context);
207
+ expect(diagnostics).toHaveLength(1);
208
+ expect(diagnostics[0].message).toContain("Inline object");
209
+ });
210
+ });
@@ -0,0 +1,70 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+ import { isInsideCompositeFactory } from "./composite-scope";
4
+
5
+ /**
6
+ * COR001: No inline objects in Declarable constructors
7
+ *
8
+ * Detects inline object literals or array literals as property values
9
+ * in Declarable constructor arguments. All structure should be expressed
10
+ * through typed Declarable declarations, never inline object literals.
11
+ *
12
+ * Triggers on: new Bucket({ bucketEncryption: { serverSideEncryptionConfiguration: [...] } })
13
+ * Triggers on: new Bucket({ tags: [{ key: "env", value: "prod" }] })
14
+ * OK: new Bucket({ bucketName: "my-bucket", accessControl: "Private" })
15
+ * OK: new Bucket({ encryption: dataEncryption })
16
+ */
17
+
18
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
19
+ // Check for NewExpression nodes (constructor calls)
20
+ // Skip resource constructors inside Composite() factory callbacks
21
+ if (ts.isNewExpression(node) && !isInsideCompositeFactory(node)) {
22
+ // Check if the first argument is an object literal
23
+ if (node.arguments && node.arguments.length > 0) {
24
+ const firstArg = node.arguments[0];
25
+
26
+ if (ts.isObjectLiteralExpression(firstArg)) {
27
+ // Check each property in the constructor argument
28
+ for (const property of firstArg.properties) {
29
+ if (ts.isPropertyAssignment(property)) {
30
+ const initializer = property.initializer;
31
+
32
+ // Flag inline object literals
33
+ // Flag array literals only if they contain inline objects/arrays
34
+ if (ts.isObjectLiteralExpression(initializer) ||
35
+ (ts.isArrayLiteralExpression(initializer) && initializer.elements.some(
36
+ el => ts.isObjectLiteralExpression(el) || ts.isArrayLiteralExpression(el)
37
+ ))) {
38
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
39
+ initializer.getStart(context.sourceFile)
40
+ );
41
+
42
+ diagnostics.push({
43
+ file: context.filePath,
44
+ line: line + 1,
45
+ column: character + 1,
46
+ ruleId: "COR001",
47
+ severity: "warning",
48
+ message: "Inline object in Declarable constructor — extract to a named 'const' with 'export'. Each config value should be its own Declarable.",
49
+ });
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // Recursively check child nodes
58
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
59
+ }
60
+
61
+ export const flatDeclarationsRule: LintRule = {
62
+ id: "COR001",
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
+ };