@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,59 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+
4
+ /**
5
+ * COR002: barrel-import-style
6
+ *
7
+ * Enforce `import * as _` for local `_.ts` barrel imports.
8
+ *
9
+ * Triggers on: import { bucketEncryption } from "./_"
10
+ * OK: import * as _ from "./_"
11
+ * OK: import type { Config } from "./_"
12
+ */
13
+
14
+ const barrelPattern = /^\.\/(_|_\..*)$/;
15
+
16
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
17
+ if (ts.isImportDeclaration(node)) {
18
+ const moduleSpecifier = node.moduleSpecifier;
19
+ if (!ts.isStringLiteral(moduleSpecifier)) return;
20
+
21
+ const modulePath = moduleSpecifier.text;
22
+ if (!barrelPattern.test(modulePath)) return;
23
+
24
+ // Skip type-only imports: import type { X } from "..."
25
+ if (node.importClause?.isTypeOnly) return;
26
+
27
+ const importClause = node.importClause;
28
+ if (!importClause?.namedBindings) return;
29
+
30
+ // Flag named imports (not namespace imports)
31
+ if (ts.isNamedImports(importClause.namedBindings)) {
32
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
33
+ node.getStart(context.sourceFile)
34
+ );
35
+
36
+ diagnostics.push({
37
+ file: context.filePath,
38
+ line: line + 1,
39
+ column: character + 1,
40
+ ruleId: "COR002",
41
+ severity: "error",
42
+ message: `Use namespace import for local barrel — replace with: import * as _ from "./_"`,
43
+ });
44
+ }
45
+ }
46
+
47
+ ts.forEachChild(node, child => checkNode(child, context, diagnostics));
48
+ }
49
+
50
+ export const barrelImportStyleRule: LintRule = {
51
+ id: "COR002",
52
+ severity: "error",
53
+ category: "style",
54
+ check(context: LintContext): LintDiagnostic[] {
55
+ const diagnostics: LintDiagnostic[] = [];
56
+ checkNode(context.sourceFile, context, diagnostics);
57
+ return diagnostics;
58
+ },
59
+ };
@@ -0,0 +1,55 @@
1
+ import * as ts from "typescript";
2
+
3
+ /**
4
+ * Check if a node is inside a Composite() factory callback.
5
+ *
6
+ * Composite factories define parameterized resource groups where dynamic
7
+ * expressions, spreads, and inline objects are expected. Lint rules that
8
+ * enforce static declarations should skip nodes inside these callbacks.
9
+ *
10
+ * Matches: Composite((props) => { ... }) and _.Composite((props) => { ... })
11
+ */
12
+ export function isInsideCompositeFactory(node: ts.Node): boolean {
13
+ let current: ts.Node | undefined = node.parent;
14
+
15
+ while (current) {
16
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
17
+ const parent = current.parent;
18
+ if (parent && ts.isCallExpression(parent) && parent.arguments[0] === current) {
19
+ const callee = parent.expression;
20
+ if (isCompositeCallee(callee)) {
21
+ return true;
22
+ }
23
+ }
24
+ }
25
+ current = current.parent;
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * If the given CallExpression is a Composite() call, return its factory argument.
33
+ * Returns undefined if not a Composite call or factory is missing.
34
+ */
35
+ export function getCompositeFactory(
36
+ call: ts.CallExpression,
37
+ ): ts.ArrowFunction | ts.FunctionExpression | undefined {
38
+ if (!isCompositeCallee(call.expression)) return undefined;
39
+ const factory = call.arguments[0];
40
+ if (!factory) return undefined;
41
+ if (ts.isArrowFunction(factory) || ts.isFunctionExpression(factory)) return factory;
42
+ return undefined;
43
+ }
44
+
45
+ export function isCompositeCallee(node: ts.Expression): boolean {
46
+ // Composite(...)
47
+ if (ts.isIdentifier(node) && node.text === "Composite") {
48
+ return true;
49
+ }
50
+ // _.Composite(...) or anything.Composite(...)
51
+ if (ts.isPropertyAccessExpression(node) && node.name.text === "Composite") {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR017: composite-name-match", () => {
12
+ test("rule metadata", () => {
13
+ expect(cor017CompositeNameMatchRule.id).toBe("COR017");
14
+ expect(cor017CompositeNameMatchRule.severity).toBe("error");
15
+ expect(cor017CompositeNameMatchRule.category).toBe("correctness");
16
+ });
17
+
18
+ test("allows matching name", () => {
19
+ const ctx = createContext(`
20
+ const LambdaApi = Composite((props) => {
21
+ return { role: new Role({}) };
22
+ }, "LambdaApi");
23
+ `);
24
+ expect(cor017CompositeNameMatchRule.check(ctx)).toHaveLength(0);
25
+ });
26
+
27
+ test("allows matching name with _.Composite", () => {
28
+ const ctx = createContext(`
29
+ const LambdaApi = _.Composite((props) => {
30
+ return { role: new Role({}) };
31
+ }, "LambdaApi");
32
+ `);
33
+ expect(cor017CompositeNameMatchRule.check(ctx)).toHaveLength(0);
34
+ });
35
+
36
+ test("flags mismatched name", () => {
37
+ const ctx = createContext(`
38
+ const LambdaApi = Composite((props) => {
39
+ return { role: new Role({}) };
40
+ }, "MyFunction");
41
+ `);
42
+ const diags = cor017CompositeNameMatchRule.check(ctx);
43
+ expect(diags).toHaveLength(1);
44
+ expect(diags[0].ruleId).toBe("COR017");
45
+ expect(diags[0].message).toContain('"MyFunction"');
46
+ expect(diags[0].message).toContain('"LambdaApi"');
47
+ expect(diags[0].fix).toBeDefined();
48
+ expect(diags[0].fix!.replacement).toBe('"LambdaApi"');
49
+ });
50
+
51
+ test("flags missing name argument", () => {
52
+ const ctx = createContext(`
53
+ const LambdaApi = Composite((props) => {
54
+ return { role: new Role({}) };
55
+ });
56
+ `);
57
+ const diags = cor017CompositeNameMatchRule.check(ctx);
58
+ expect(diags).toHaveLength(1);
59
+ expect(diags[0].ruleId).toBe("COR017");
60
+ expect(diags[0].message).toContain("missing a name argument");
61
+ expect(diags[0].message).toContain('"LambdaApi"');
62
+ expect(diags[0].fix).toBeDefined();
63
+ expect(diags[0].fix!.replacement).toBe(', "LambdaApi"');
64
+ });
65
+
66
+ test("flags non-literal name argument", () => {
67
+ const ctx = createContext(`
68
+ const LambdaApi = Composite((props) => {
69
+ return { role: new Role({}) };
70
+ }, getName());
71
+ `);
72
+ const diags = cor017CompositeNameMatchRule.check(ctx);
73
+ expect(diags).toHaveLength(1);
74
+ expect(diags[0].message).toContain("string literal");
75
+ expect(diags[0].fix).toBeDefined();
76
+ expect(diags[0].fix!.replacement).toBe('"LambdaApi"');
77
+ });
78
+
79
+ test("does not flag non-Composite calls", () => {
80
+ const ctx = createContext(`
81
+ const thing = createThing((props) => {
82
+ return { role: new Role({}) };
83
+ }, "wrong");
84
+ `);
85
+ expect(cor017CompositeNameMatchRule.check(ctx)).toHaveLength(0);
86
+ });
87
+
88
+ test("handles _.Composite with mismatch", () => {
89
+ const ctx = createContext(`
90
+ const SecureStorage = _.Composite((props) => {
91
+ return { bucket: new Bucket({}) };
92
+ }, "Storage");
93
+ `);
94
+ const diags = cor017CompositeNameMatchRule.check(ctx);
95
+ expect(diags).toHaveLength(1);
96
+ expect(diags[0].fix!.replacement).toBe('"SecureStorage"');
97
+ });
98
+
99
+ test("handles export const pattern", () => {
100
+ const ctx = createContext(`
101
+ export const LambdaApi = Composite((props) => {
102
+ return { role: new Role({}) };
103
+ }, "LambdaApi");
104
+ `);
105
+ expect(cor017CompositeNameMatchRule.check(ctx)).toHaveLength(0);
106
+ });
107
+ });
@@ -0,0 +1,108 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+ import { isCompositeCallee } from "./composite-scope";
4
+
5
+ /**
6
+ * COR017: Composite name must match variable name
7
+ *
8
+ * The second argument to Composite() (the name string) must match the
9
+ * const variable it's assigned to. This name is used in resource expansion
10
+ * (e.g. healthApi_role, healthApi_func) and in error messages.
11
+ *
12
+ * Triggers on: const LambdaApi = Composite(fn, "MyFunction") — mismatch
13
+ * Triggers on: const LambdaApi = Composite(fn) — missing name
14
+ * OK: const LambdaApi = Composite(fn, "LambdaApi")
15
+ */
16
+
17
+ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
18
+ if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
19
+ const call = node.initializer;
20
+ if (!isCompositeCallee(call.expression)) {
21
+ ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
22
+ return;
23
+ }
24
+
25
+ // Get the variable name
26
+ if (!ts.isIdentifier(node.name)) {
27
+ ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
28
+ return;
29
+ }
30
+ const variableName = node.name.text;
31
+
32
+ // Check second argument
33
+ const nameArg = call.arguments[1];
34
+
35
+ if (!nameArg) {
36
+ // Missing name argument
37
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
38
+ call.expression.getStart(context.sourceFile),
39
+ );
40
+
41
+ // Insert fix: add name argument after the factory callback
42
+ const factoryArg = call.arguments[0];
43
+ const insertPos = factoryArg.getEnd();
44
+
45
+ diagnostics.push({
46
+ file: context.filePath,
47
+ line: line + 1,
48
+ column: character + 1,
49
+ ruleId: "COR017",
50
+ severity: "error",
51
+ message: `Composite is missing a name argument — add "${variableName}" as the second argument`,
52
+ fix: {
53
+ range: [insertPos, insertPos],
54
+ replacement: `, "${variableName}"`,
55
+ },
56
+ });
57
+ } else if (!ts.isStringLiteral(nameArg)) {
58
+ // Non-literal name argument
59
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
60
+ nameArg.getStart(context.sourceFile),
61
+ );
62
+
63
+ diagnostics.push({
64
+ file: context.filePath,
65
+ line: line + 1,
66
+ column: character + 1,
67
+ ruleId: "COR017",
68
+ severity: "error",
69
+ message: `Composite name must be a string literal`,
70
+ fix: {
71
+ range: [nameArg.getStart(context.sourceFile), nameArg.getEnd()],
72
+ replacement: `"${variableName}"`,
73
+ },
74
+ });
75
+ } else if (nameArg.text !== variableName) {
76
+ // Name mismatch
77
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
78
+ nameArg.getStart(context.sourceFile),
79
+ );
80
+
81
+ diagnostics.push({
82
+ file: context.filePath,
83
+ line: line + 1,
84
+ column: character + 1,
85
+ ruleId: "COR017",
86
+ severity: "error",
87
+ message: `Composite name "${nameArg.text}" does not match variable name "${variableName}"`,
88
+ fix: {
89
+ range: [nameArg.getStart(context.sourceFile), nameArg.getEnd()],
90
+ replacement: `"${variableName}"`,
91
+ },
92
+ });
93
+ }
94
+ }
95
+
96
+ ts.forEachChild(node, (child) => checkNode(child, context, diagnostics));
97
+ }
98
+
99
+ export const cor017CompositeNameMatchRule: LintRule = {
100
+ id: "COR017",
101
+ severity: "error",
102
+ category: "correctness",
103
+ check(context: LintContext): LintDiagnostic[] {
104
+ const diagnostics: LintDiagnostic[] = [];
105
+ checkNode(context.sourceFile, context, diagnostics);
106
+ return diagnostics;
107
+ },
108
+ };
@@ -0,0 +1,172 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
4
+ import type { LintContext } from "../rule";
5
+
6
+ function createContext(code: string, filePath = "test.ts"): LintContext {
7
+ const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath, lexicon: undefined };
9
+ }
10
+
11
+ describe("COR018: composite-prefer-lexicon-type", () => {
12
+ test("rule metadata", () => {
13
+ expect(cor018CompositePreferLexiconTypeRule.id).toBe("COR018");
14
+ expect(cor018CompositePreferLexiconTypeRule.severity).toBe("info");
15
+ expect(cor018CompositePreferLexiconTypeRule.category).toBe("style");
16
+ });
17
+
18
+ test("flags local interface used in Composite props", () => {
19
+ const ctx = createContext(`
20
+ interface PolicyStatement {
21
+ effect: string;
22
+ action: string[];
23
+ resource: string[];
24
+ }
25
+
26
+ interface LambdaApiProps {
27
+ name: string;
28
+ policies?: PolicyStatement[];
29
+ }
30
+
31
+ const LambdaApi = Composite<LambdaApiProps>((props) => {
32
+ const role = new Role({ policies: props.policies });
33
+ return { role };
34
+ }, "LambdaApi");
35
+ `);
36
+ const diags = cor018CompositePreferLexiconTypeRule.check(ctx);
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].ruleId).toBe("COR018");
39
+ expect(diags[0].message).toContain("PolicyStatement");
40
+ expect(diags[0].message).toContain("policies");
41
+ expect(diags[0].message).toContain("lexicon property type");
42
+ });
43
+
44
+ test("does not flag when no local types are used", () => {
45
+ const ctx = createContext(`
46
+ interface LambdaApiProps {
47
+ name: string;
48
+ timeout?: number;
49
+ policies?: InstanceType<typeof Role_Policy>[];
50
+ }
51
+
52
+ const LambdaApi = Composite<LambdaApiProps>((props) => {
53
+ const role = new Role({ policies: props.policies });
54
+ return { role };
55
+ }, "LambdaApi");
56
+ `);
57
+ expect(cor018CompositePreferLexiconTypeRule.check(ctx)).toHaveLength(0);
58
+ });
59
+
60
+ test("does not flag files without Composite", () => {
61
+ const ctx = createContext(`
62
+ interface MyType {
63
+ name: string;
64
+ }
65
+
66
+ interface MyProps {
67
+ item: MyType;
68
+ }
69
+ `);
70
+ expect(cor018CompositePreferLexiconTypeRule.check(ctx)).toHaveLength(0);
71
+ });
72
+
73
+ test("flags local type alias used in Composite props", () => {
74
+ const ctx = createContext(`
75
+ type EncryptionConfig = {
76
+ algorithm: string;
77
+ keyId: string;
78
+ };
79
+
80
+ interface StorageProps {
81
+ encryption: EncryptionConfig;
82
+ }
83
+
84
+ const Storage = Composite<StorageProps>((props) => {
85
+ const bucket = new Bucket({ encryption: props.encryption });
86
+ return { bucket };
87
+ }, "Storage");
88
+ `);
89
+ const diags = cor018CompositePreferLexiconTypeRule.check(ctx);
90
+ expect(diags).toHaveLength(1);
91
+ expect(diags[0].message).toContain("EncryptionConfig");
92
+ expect(diags[0].message).toContain("encryption");
93
+ });
94
+
95
+ test("flags with _.Composite", () => {
96
+ const ctx = createContext(`
97
+ interface CustomPolicy {
98
+ name: string;
99
+ }
100
+
101
+ interface MyProps {
102
+ policy: CustomPolicy;
103
+ }
104
+
105
+ const MyComp = _.Composite<MyProps>((props) => {
106
+ return { role: new Role({}) };
107
+ }, "MyComp");
108
+ `);
109
+ const diags = cor018CompositePreferLexiconTypeRule.check(ctx);
110
+ expect(diags).toHaveLength(1);
111
+ expect(diags[0].message).toContain("CustomPolicy");
112
+ });
113
+
114
+ test("flags multiple local types in props", () => {
115
+ const ctx = createContext(`
116
+ interface PolicyDoc {
117
+ version: string;
118
+ }
119
+
120
+ interface TagConfig {
121
+ key: string;
122
+ value: string;
123
+ }
124
+
125
+ interface MyProps {
126
+ policy: PolicyDoc;
127
+ tags: TagConfig[];
128
+ }
129
+
130
+ const MyComp = Composite<MyProps>((props) => {
131
+ return { role: new Role({}) };
132
+ }, "MyComp");
133
+ `);
134
+ const diags = cor018CompositePreferLexiconTypeRule.check(ctx);
135
+ expect(diags).toHaveLength(2);
136
+ });
137
+
138
+ test("does not flag primitives in props", () => {
139
+ const ctx = createContext(`
140
+ interface MyProps {
141
+ name: string;
142
+ count: number;
143
+ enabled: boolean;
144
+ tags: string[];
145
+ }
146
+
147
+ const MyComp = Composite<MyProps>((props) => {
148
+ return { bucket: new Bucket({}) };
149
+ }, "MyComp");
150
+ `);
151
+ expect(cor018CompositePreferLexiconTypeRule.check(ctx)).toHaveLength(0);
152
+ });
153
+
154
+ test("handles union types with local type", () => {
155
+ const ctx = createContext(`
156
+ interface CustomCode {
157
+ zipFile: string;
158
+ }
159
+
160
+ interface MyProps {
161
+ code: CustomCode | string;
162
+ }
163
+
164
+ const MyComp = Composite<MyProps>((props) => {
165
+ return { func: new Function({}) };
166
+ }, "MyComp");
167
+ `);
168
+ const diags = cor018CompositePreferLexiconTypeRule.check(ctx);
169
+ expect(diags).toHaveLength(1);
170
+ expect(diags[0].message).toContain("CustomCode");
171
+ });
172
+ });
@@ -0,0 +1,167 @@
1
+ import * as ts from "typescript";
2
+ import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+ import { isCompositeCallee } from "./composite-scope";
4
+
5
+ /**
6
+ * COR018: Prefer lexicon property types in Composite props
7
+ *
8
+ * Composite prop interfaces should use lexicon property types (via the barrel)
9
+ * instead of locally-declared interfaces or type aliases. Local types next to
10
+ * a Composite definition often duplicate existing lexicon property types.
11
+ *
12
+ * Triggers on: interface with fields used as a Composite prop type, declared
13
+ * in the same file as the Composite (excluding the props interface itself)
14
+ * OK: InstanceType<typeof _.Role_Policy>
15
+ * OK: primitives (string, number, boolean)
16
+ * OK: barrel-imported types
17
+ */
18
+
19
+ /**
20
+ * Find all Composite() calls and extract their props type names.
21
+ */
22
+ function findCompositePropsTypes(sourceFile: ts.SourceFile): Set<string> {
23
+ const propsTypes = new Set<string>();
24
+
25
+ function walk(node: ts.Node): void {
26
+ if (ts.isCallExpression(node) && isCompositeCallee(node.expression)) {
27
+ // Composite<PropsType>(...) — extract from type arguments
28
+ if (node.typeArguments && node.typeArguments.length > 0) {
29
+ const typeArg = node.typeArguments[0];
30
+ if (ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName)) {
31
+ propsTypes.add(typeArg.typeName.text);
32
+ }
33
+ }
34
+ }
35
+ ts.forEachChild(node, walk);
36
+ }
37
+
38
+ walk(sourceFile);
39
+ return propsTypes;
40
+ }
41
+
42
+ /**
43
+ * Find all locally-declared interface and type alias names.
44
+ */
45
+ function findLocalTypeDeclarations(sourceFile: ts.SourceFile): Map<string, ts.Node> {
46
+ const types = new Map<string, ts.Node>();
47
+
48
+ function walk(node: ts.Node): void {
49
+ if (ts.isInterfaceDeclaration(node)) {
50
+ types.set(node.name.text, node);
51
+ } else if (ts.isTypeAliasDeclaration(node)) {
52
+ types.set(node.name.text, node);
53
+ }
54
+ ts.forEachChild(node, walk);
55
+ }
56
+
57
+ walk(sourceFile);
58
+ return types;
59
+ }
60
+
61
+ /**
62
+ * Collect type references used in a props interface's field types.
63
+ * Returns names of locally-referenced types (not primitives, not from barrel).
64
+ */
65
+ function collectFieldTypeRefs(
66
+ propsDecl: ts.Node,
67
+ localTypes: Map<string, ts.Node>,
68
+ ): Map<string, { fieldName: string; typeNode: ts.Node }> {
69
+ const refs = new Map<string, { fieldName: string; typeNode: ts.Node }>();
70
+
71
+ if (!ts.isInterfaceDeclaration(propsDecl)) return refs;
72
+
73
+ for (const member of propsDecl.members) {
74
+ if (!ts.isPropertySignature(member) || !member.type) continue;
75
+
76
+ const fieldName = ts.isIdentifier(member.name!) ? member.name!.text : "<computed>";
77
+ collectTypeRefsFromNode(member.type, localTypes, fieldName, refs);
78
+ }
79
+
80
+ return refs;
81
+ }
82
+
83
+ function collectTypeRefsFromNode(
84
+ node: ts.TypeNode,
85
+ localTypes: Map<string, ts.Node>,
86
+ fieldName: string,
87
+ refs: Map<string, { fieldName: string; typeNode: ts.Node }>,
88
+ ): void {
89
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
90
+ const name = node.typeName.text;
91
+ if (localTypes.has(name) && !refs.has(name)) {
92
+ refs.set(name, { fieldName, typeNode: node });
93
+ }
94
+ }
95
+
96
+ // Recurse into array types: CustomType[] → CustomType
97
+ if (ts.isArrayTypeNode(node)) {
98
+ collectTypeRefsFromNode(node.elementType, localTypes, fieldName, refs);
99
+ }
100
+
101
+ // Recurse into union/intersection types
102
+ if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
103
+ for (const member of node.types) {
104
+ collectTypeRefsFromNode(member, localTypes, fieldName, refs);
105
+ }
106
+ }
107
+
108
+ // Recurse into generic type arguments
109
+ if (ts.isTypeReferenceNode(node) && node.typeArguments) {
110
+ for (const arg of node.typeArguments) {
111
+ collectTypeRefsFromNode(arg, localTypes, fieldName, refs);
112
+ }
113
+ }
114
+ }
115
+
116
+ function checkFile(context: LintContext): LintDiagnostic[] {
117
+ const diagnostics: LintDiagnostic[] = [];
118
+ const { sourceFile } = context;
119
+
120
+ // Find Composite calls and their props types
121
+ const propsTypeNames = findCompositePropsTypes(sourceFile);
122
+ if (propsTypeNames.size === 0) return diagnostics;
123
+
124
+ // Find all local type declarations
125
+ const localTypes = findLocalTypeDeclarations(sourceFile);
126
+
127
+ for (const propsTypeName of propsTypeNames) {
128
+ const propsDecl = localTypes.get(propsTypeName);
129
+ if (!propsDecl) continue;
130
+
131
+ // Find local types referenced in the props fields
132
+ // Exclude the props interface itself
133
+ const otherLocalTypes = new Map(localTypes);
134
+ otherLocalTypes.delete(propsTypeName);
135
+
136
+ if (otherLocalTypes.size === 0) continue;
137
+
138
+ const fieldRefs = collectFieldTypeRefs(propsDecl, otherLocalTypes);
139
+
140
+ for (const [typeName, { fieldName, typeNode }] of fieldRefs) {
141
+ const typeDecl = localTypes.get(typeName)!;
142
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
143
+ typeDecl.getStart(context.sourceFile),
144
+ );
145
+
146
+ diagnostics.push({
147
+ file: context.filePath,
148
+ line: line + 1,
149
+ column: character + 1,
150
+ ruleId: "COR018",
151
+ severity: "info",
152
+ message: `Local type "${typeName}" is used in Composite prop "${fieldName}" — consider using a lexicon property type instead (e.g., InstanceType<typeof _.PropertyType>)`,
153
+ });
154
+ }
155
+ }
156
+
157
+ return diagnostics;
158
+ }
159
+
160
+ export const cor018CompositePreferLexiconTypeRule: LintRule = {
161
+ id: "COR018",
162
+ severity: "info",
163
+ category: "style",
164
+ check(context: LintContext): LintDiagnostic[] {
165
+ return checkFile(context);
166
+ },
167
+ };