@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,476 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildDependencyGraph } from "./graph";
3
+ import { DECLARABLE_MARKER, type Declarable } from "../declarable";
4
+ import { AttrRef } from "../attrref";
5
+
6
+ describe("buildDependencyGraph", () => {
7
+ test("returns empty graph for empty entities map", () => {
8
+ const entities = new Map<string, Declarable>();
9
+ const graph = buildDependencyGraph(entities);
10
+ expect(graph.size).toBe(0);
11
+ });
12
+
13
+ test("returns graph with no dependencies for single entity", () => {
14
+ const entity: Declarable = {
15
+ entityType: "test",
16
+ [DECLARABLE_MARKER]: true,
17
+ };
18
+
19
+ const entities = new Map([["Entity1", entity]]);
20
+ const graph = buildDependencyGraph(entities);
21
+
22
+ expect(graph.size).toBe(1);
23
+ expect(graph.get("Entity1")?.size).toBe(0);
24
+ });
25
+
26
+ test("returns graph with no dependencies for multiple unrelated entities", () => {
27
+ const entity1: Declarable = {
28
+ entityType: "test",
29
+ [DECLARABLE_MARKER]: true,
30
+ };
31
+
32
+ const entity2: Declarable = {
33
+ entityType: "test",
34
+ [DECLARABLE_MARKER]: true,
35
+ };
36
+
37
+ const entities = new Map([
38
+ ["Entity1", entity1],
39
+ ["Entity2", entity2],
40
+ ]);
41
+ const graph = buildDependencyGraph(entities);
42
+
43
+ expect(graph.size).toBe(2);
44
+ expect(graph.get("Entity1")?.size).toBe(0);
45
+ expect(graph.get("Entity2")?.size).toBe(0);
46
+ });
47
+
48
+ test("detects dependency from AttrRef", () => {
49
+ const parent: Declarable = {
50
+ entityType: "parent",
51
+ [DECLARABLE_MARKER]: true,
52
+ };
53
+
54
+ const child: Declarable & { ref: AttrRef } = {
55
+ entityType: "child",
56
+ [DECLARABLE_MARKER]: true,
57
+ ref: new AttrRef(parent, "someAttr"),
58
+ };
59
+
60
+ const entities = new Map([
61
+ ["Parent", parent],
62
+ ["Child", child],
63
+ ]);
64
+ const graph = buildDependencyGraph(entities);
65
+
66
+ expect(graph.size).toBe(2);
67
+ expect(graph.get("Parent")?.size).toBe(0);
68
+ expect(graph.get("Child")?.has("Parent")).toBe(true);
69
+ expect(graph.get("Child")?.size).toBe(1);
70
+ });
71
+
72
+ test("detects dependency from direct Declarable reference", () => {
73
+ const entity1: Declarable = {
74
+ entityType: "type1",
75
+ [DECLARABLE_MARKER]: true,
76
+ };
77
+
78
+ const entity2: Declarable & { dependency: Declarable } = {
79
+ entityType: "type2",
80
+ [DECLARABLE_MARKER]: true,
81
+ dependency: entity1,
82
+ };
83
+
84
+ const entities = new Map([
85
+ ["Entity1", entity1],
86
+ ["Entity2", entity2],
87
+ ]);
88
+ const graph = buildDependencyGraph(entities);
89
+
90
+ expect(graph.size).toBe(2);
91
+ expect(graph.get("Entity1")?.size).toBe(0);
92
+ expect(graph.get("Entity2")?.has("Entity1")).toBe(true);
93
+ expect(graph.get("Entity2")?.size).toBe(1);
94
+ });
95
+
96
+ test("detects multiple dependencies from one entity", () => {
97
+ const entity1: Declarable = {
98
+ entityType: "type1",
99
+ [DECLARABLE_MARKER]: true,
100
+ };
101
+
102
+ const entity2: Declarable = {
103
+ entityType: "type2",
104
+ [DECLARABLE_MARKER]: true,
105
+ };
106
+
107
+ const entity3: Declarable & { dep1: Declarable; dep2: Declarable } = {
108
+ entityType: "type3",
109
+ [DECLARABLE_MARKER]: true,
110
+ dep1: entity1,
111
+ dep2: entity2,
112
+ };
113
+
114
+ const entities = new Map([
115
+ ["Entity1", entity1],
116
+ ["Entity2", entity2],
117
+ ["Entity3", entity3],
118
+ ]);
119
+ const graph = buildDependencyGraph(entities);
120
+
121
+ expect(graph.size).toBe(3);
122
+ expect(graph.get("Entity3")?.has("Entity1")).toBe(true);
123
+ expect(graph.get("Entity3")?.has("Entity2")).toBe(true);
124
+ expect(graph.get("Entity3")?.size).toBe(2);
125
+ });
126
+
127
+ test("detects dependencies in nested objects", () => {
128
+ const entity1: Declarable = {
129
+ entityType: "type1",
130
+ [DECLARABLE_MARKER]: true,
131
+ };
132
+
133
+ const entity2: Declarable & { nested: { deep: Declarable } } = {
134
+ entityType: "type2",
135
+ [DECLARABLE_MARKER]: true,
136
+ nested: {
137
+ deep: entity1,
138
+ },
139
+ };
140
+
141
+ const entities = new Map([
142
+ ["Entity1", entity1],
143
+ ["Entity2", entity2],
144
+ ]);
145
+ const graph = buildDependencyGraph(entities);
146
+
147
+ expect(graph.get("Entity2")?.has("Entity1")).toBe(true);
148
+ expect(graph.get("Entity2")?.size).toBe(1);
149
+ });
150
+
151
+ test("detects dependencies in arrays", () => {
152
+ const entity1: Declarable = {
153
+ entityType: "type1",
154
+ [DECLARABLE_MARKER]: true,
155
+ };
156
+
157
+ const entity2: Declarable = {
158
+ entityType: "type2",
159
+ [DECLARABLE_MARKER]: true,
160
+ };
161
+
162
+ const entity3: Declarable & { deps: Declarable[] } = {
163
+ entityType: "type3",
164
+ [DECLARABLE_MARKER]: true,
165
+ deps: [entity1, entity2],
166
+ };
167
+
168
+ const entities = new Map([
169
+ ["Entity1", entity1],
170
+ ["Entity2", entity2],
171
+ ["Entity3", entity3],
172
+ ]);
173
+ const graph = buildDependencyGraph(entities);
174
+
175
+ expect(graph.get("Entity3")?.has("Entity1")).toBe(true);
176
+ expect(graph.get("Entity3")?.has("Entity2")).toBe(true);
177
+ expect(graph.get("Entity3")?.size).toBe(2);
178
+ });
179
+
180
+ test("detects mixed AttrRef and Declarable dependencies", () => {
181
+ const entity1: Declarable = {
182
+ entityType: "type1",
183
+ [DECLARABLE_MARKER]: true,
184
+ };
185
+
186
+ const entity2: Declarable = {
187
+ entityType: "type2",
188
+ [DECLARABLE_MARKER]: true,
189
+ };
190
+
191
+ const entity3: Declarable & { ref: AttrRef; dep: Declarable } = {
192
+ entityType: "type3",
193
+ [DECLARABLE_MARKER]: true,
194
+ ref: new AttrRef(entity1, "attr"),
195
+ dep: entity2,
196
+ };
197
+
198
+ const entities = new Map([
199
+ ["Entity1", entity1],
200
+ ["Entity2", entity2],
201
+ ["Entity3", entity3],
202
+ ]);
203
+ const graph = buildDependencyGraph(entities);
204
+
205
+ expect(graph.get("Entity3")?.has("Entity1")).toBe(true);
206
+ expect(graph.get("Entity3")?.has("Entity2")).toBe(true);
207
+ expect(graph.get("Entity3")?.size).toBe(2);
208
+ });
209
+
210
+ test("handles transitive dependencies correctly", () => {
211
+ const entity1: Declarable = {
212
+ entityType: "type1",
213
+ [DECLARABLE_MARKER]: true,
214
+ };
215
+
216
+ const entity2: Declarable & { dep: Declarable } = {
217
+ entityType: "type2",
218
+ [DECLARABLE_MARKER]: true,
219
+ dep: entity1,
220
+ };
221
+
222
+ const entity3: Declarable & { dep: Declarable } = {
223
+ entityType: "type3",
224
+ [DECLARABLE_MARKER]: true,
225
+ dep: entity2,
226
+ };
227
+
228
+ const entities = new Map([
229
+ ["Entity1", entity1],
230
+ ["Entity2", entity2],
231
+ ["Entity3", entity3],
232
+ ]);
233
+ const graph = buildDependencyGraph(entities);
234
+
235
+ expect(graph.get("Entity1")?.size).toBe(0);
236
+ expect(graph.get("Entity2")?.has("Entity1")).toBe(true);
237
+ expect(graph.get("Entity2")?.size).toBe(1);
238
+ expect(graph.get("Entity3")?.has("Entity2")).toBe(true);
239
+ expect(graph.get("Entity3")?.size).toBe(1);
240
+ });
241
+
242
+ test("ignores non-entity declarables", () => {
243
+ const entity: Declarable = {
244
+ entityType: "test",
245
+ [DECLARABLE_MARKER]: true,
246
+ };
247
+
248
+ const notInEntities: Declarable = {
249
+ entityType: "external",
250
+ [DECLARABLE_MARKER]: true,
251
+ };
252
+
253
+ const entityWithExternal: Declarable & { dep: Declarable } = {
254
+ entityType: "test",
255
+ [DECLARABLE_MARKER]: true,
256
+ dep: notInEntities,
257
+ };
258
+
259
+ const entities = new Map([
260
+ ["Entity", entity],
261
+ ["EntityWithExternal", entityWithExternal],
262
+ ]);
263
+ const graph = buildDependencyGraph(entities);
264
+
265
+ expect(graph.get("EntityWithExternal")?.size).toBe(0);
266
+ });
267
+
268
+ test("ignores AttrRef with parent not in entities", () => {
269
+ const externalParent: Declarable = {
270
+ entityType: "external",
271
+ [DECLARABLE_MARKER]: true,
272
+ };
273
+
274
+ const entity: Declarable & { ref: AttrRef } = {
275
+ entityType: "test",
276
+ [DECLARABLE_MARKER]: true,
277
+ ref: new AttrRef(externalParent, "attr"),
278
+ };
279
+
280
+ const entities = new Map([["Entity", entity]]);
281
+ const graph = buildDependencyGraph(entities);
282
+
283
+ expect(graph.get("Entity")?.size).toBe(0);
284
+ });
285
+
286
+ test("handles circular references without infinite loop", () => {
287
+ const entity1: Declarable & { other?: Declarable } = {
288
+ entityType: "type1",
289
+ [DECLARABLE_MARKER]: true,
290
+ };
291
+
292
+ const entity2: Declarable & { other: Declarable } = {
293
+ entityType: "type2",
294
+ [DECLARABLE_MARKER]: true,
295
+ other: entity1,
296
+ };
297
+
298
+ entity1.other = entity2;
299
+
300
+ const entities = new Map([
301
+ ["Entity1", entity1],
302
+ ["Entity2", entity2],
303
+ ]);
304
+ const graph = buildDependencyGraph(entities);
305
+
306
+ expect(graph.get("Entity1")?.has("Entity2")).toBe(true);
307
+ expect(graph.get("Entity2")?.has("Entity1")).toBe(true);
308
+ });
309
+
310
+ test("handles self-reference without infinite loop", () => {
311
+ const entity: Declarable & { self?: Declarable } = {
312
+ entityType: "test",
313
+ [DECLARABLE_MARKER]: true,
314
+ };
315
+
316
+ entity.self = entity;
317
+
318
+ const entities = new Map([["Entity", entity]]);
319
+ const graph = buildDependencyGraph(entities);
320
+
321
+ expect(graph.get("Entity")?.has("Entity")).toBe(true);
322
+ expect(graph.get("Entity")?.size).toBe(1);
323
+ });
324
+
325
+ test("ignores primitive values", () => {
326
+ const entity: Declarable & {
327
+ str: string;
328
+ num: number;
329
+ bool: boolean;
330
+ nul: null;
331
+ } = {
332
+ entityType: "test",
333
+ [DECLARABLE_MARKER]: true,
334
+ str: "value",
335
+ num: 42,
336
+ bool: true,
337
+ nul: null,
338
+ };
339
+
340
+ const entities = new Map([["Entity", entity]]);
341
+ const graph = buildDependencyGraph(entities);
342
+
343
+ expect(graph.get("Entity")?.size).toBe(0);
344
+ });
345
+
346
+ test("self-referencing AttrRefs do not create self-dependency", () => {
347
+ // Mirrors createResource: each resource has AttrRef properties whose
348
+ // parent is the resource itself (e.g. bucket.arn, bucket.bucketName).
349
+ // These are not real dependencies — they're just attribute accessors.
350
+ const resource: Declarable & { arn?: AttrRef; bucketName?: AttrRef } = {
351
+ entityType: "AWS::S3::Bucket",
352
+ [DECLARABLE_MARKER]: true,
353
+ };
354
+ resource.arn = new AttrRef(resource, "Arn");
355
+ resource.bucketName = new AttrRef(resource, "BucketName");
356
+
357
+ const entities = new Map([["myBucket", resource]]);
358
+ const graph = buildDependencyGraph(entities);
359
+
360
+ expect(graph.get("myBucket")?.size).toBe(0);
361
+ });
362
+
363
+ test("self-referencing AttrRefs do not mask real cross-resource deps", () => {
364
+ // A resource has its own AttrRefs (self-pointing) AND a property that
365
+ // references a different entity. Only the cross-resource dep should appear.
366
+ const defaults: Declarable = {
367
+ entityType: "AWS::S3::VersioningConfiguration",
368
+ [DECLARABLE_MARKER]: true,
369
+ };
370
+
371
+ const bucket: Declarable & {
372
+ arn?: AttrRef;
373
+ versioningConfiguration?: Declarable;
374
+ } = {
375
+ entityType: "AWS::S3::Bucket",
376
+ [DECLARABLE_MARKER]: true,
377
+ };
378
+ bucket.arn = new AttrRef(bucket, "Arn");
379
+ bucket.versioningConfiguration = defaults;
380
+
381
+ const entities = new Map([
382
+ ["defaults", defaults],
383
+ ["myBucket", bucket],
384
+ ]);
385
+ const graph = buildDependencyGraph(entities);
386
+
387
+ expect(graph.get("myBucket")?.has("defaults")).toBe(true);
388
+ expect(graph.get("myBucket")?.has("myBucket")).toBe(false);
389
+ expect(graph.get("myBucket")?.size).toBe(1);
390
+ });
391
+
392
+ test("ignores plain objects without markers", () => {
393
+ const entity: Declarable & { data: { key: string } } = {
394
+ entityType: "test",
395
+ [DECLARABLE_MARKER]: true,
396
+ data: { key: "value" },
397
+ };
398
+
399
+ const entities = new Map([["Entity", entity]]);
400
+ const graph = buildDependencyGraph(entities);
401
+
402
+ expect(graph.get("Entity")?.size).toBe(0);
403
+ });
404
+
405
+ test("handles AttrRef with garbage collected parent gracefully", () => {
406
+ const entity: Declarable & { ref: AttrRef } = {
407
+ entityType: "test",
408
+ [DECLARABLE_MARKER]: true,
409
+ ref: new AttrRef({}, "attr"), // Using plain object that will be GC'd
410
+ };
411
+
412
+ const entities = new Map([["Entity", entity]]);
413
+ const graph = buildDependencyGraph(entities);
414
+
415
+ // Should not crash and should have no dependencies
416
+ expect(graph.get("Entity")?.size).toBe(0);
417
+ });
418
+
419
+ test("detects dependencies deeply nested in arrays and objects", () => {
420
+ const entity1: Declarable = {
421
+ entityType: "type1",
422
+ [DECLARABLE_MARKER]: true,
423
+ };
424
+
425
+ const entity2: Declarable & {
426
+ complex: { nested: { array: Array<{ item: Declarable }> } };
427
+ } = {
428
+ entityType: "type2",
429
+ [DECLARABLE_MARKER]: true,
430
+ complex: {
431
+ nested: {
432
+ array: [{ item: entity1 }],
433
+ },
434
+ },
435
+ };
436
+
437
+ const entities = new Map([
438
+ ["Entity1", entity1],
439
+ ["Entity2", entity2],
440
+ ]);
441
+ const graph = buildDependencyGraph(entities);
442
+
443
+ expect(graph.get("Entity2")?.has("Entity1")).toBe(true);
444
+ expect(graph.get("Entity2")?.size).toBe(1);
445
+ });
446
+
447
+ test("does not traverse into referenced declarables", () => {
448
+ const entity1: Declarable = {
449
+ entityType: "type1",
450
+ [DECLARABLE_MARKER]: true,
451
+ };
452
+
453
+ const entity2: Declarable & { internal: { data: string } } = {
454
+ entityType: "type2",
455
+ [DECLARABLE_MARKER]: true,
456
+ internal: { data: "should not traverse this" },
457
+ };
458
+
459
+ const entity3: Declarable & { dep: Declarable } = {
460
+ entityType: "type3",
461
+ [DECLARABLE_MARKER]: true,
462
+ dep: entity2,
463
+ };
464
+
465
+ const entities = new Map([
466
+ ["Entity1", entity1],
467
+ ["Entity2", entity2],
468
+ ["Entity3", entity3],
469
+ ]);
470
+ const graph = buildDependencyGraph(entities);
471
+
472
+ // Entity3 should only depend on Entity2, not traverse into Entity2's properties
473
+ expect(graph.get("Entity3")?.has("Entity2")).toBe(true);
474
+ expect(graph.get("Entity3")?.size).toBe(1);
475
+ });
476
+ });
@@ -0,0 +1,150 @@
1
+ import type { Declarable } from "../declarable";
2
+ import { isDeclarable } from "../declarable";
3
+ import { AttrRef } from "../attrref";
4
+
5
+ /**
6
+ * Builds a dependency graph from a collection of entities
7
+ * @param entities - Map of export name to Declarable entity
8
+ * @returns Map of entity name to set of entity names it depends on
9
+ */
10
+ export function buildDependencyGraph(
11
+ entities: Map<string, Declarable>
12
+ ): Map<string, Set<string>> {
13
+ const graph = new Map<string, Set<string>>();
14
+
15
+ // Initialize graph with all entity names
16
+ for (const name of entities.keys()) {
17
+ graph.set(name, new Set<string>());
18
+ }
19
+
20
+ // Build reverse lookup: entity -> name
21
+ const entityToName = new Map<Declarable, string>();
22
+ for (const [name, entity] of entities.entries()) {
23
+ entityToName.set(entity, name);
24
+ }
25
+
26
+ // Find dependencies for each entity
27
+ for (const [name, entity] of entities.entries()) {
28
+ const dependencies = graph.get(name)!;
29
+ const visited = new Set<unknown>();
30
+ // Scan the root entity's properties
31
+ scanProperties(entity, entities, entityToName, dependencies, visited, entity);
32
+ }
33
+
34
+ return graph;
35
+ }
36
+
37
+ /**
38
+ * Scans properties of an object for dependencies
39
+ */
40
+ function scanProperties(
41
+ obj: object,
42
+ entities: Map<string, Declarable>,
43
+ entityToName: Map<Declarable, string>,
44
+ dependencies: Set<string>,
45
+ visited: Set<unknown>,
46
+ rootEntity: Declarable
47
+ ): void {
48
+ // Mark this object as visited (but not if it's a Declarable, we'll handle that in findDependencies)
49
+ if (!isDeclarable(obj) && !visited.has(obj)) {
50
+ visited.add(obj);
51
+ }
52
+
53
+ // Recurse into arrays
54
+ if (Array.isArray(obj)) {
55
+ for (const item of obj) {
56
+ findDependencies(item, entities, entityToName, dependencies, visited, rootEntity);
57
+ }
58
+ return;
59
+ }
60
+
61
+ // Recurse into object properties
62
+ for (const prop in obj) {
63
+ if (Object.prototype.hasOwnProperty.call(obj, prop)) {
64
+ const propValue = (obj as Record<string, unknown>)[prop];
65
+ findDependencies(
66
+ propValue,
67
+ entities,
68
+ entityToName,
69
+ dependencies,
70
+ visited,
71
+ rootEntity
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Recursively finds all dependencies in a value
79
+ */
80
+ function findDependencies(
81
+ value: unknown,
82
+ entities: Map<string, Declarable>,
83
+ entityToName: Map<Declarable, string>,
84
+ dependencies: Set<string>,
85
+ visited: Set<unknown>,
86
+ rootEntity: Declarable
87
+ ): void {
88
+ // Avoid infinite recursion on circular references
89
+ if (value === null || value === undefined) {
90
+ return;
91
+ }
92
+
93
+ // For primitives, nothing to do
94
+ if (typeof value !== "object") {
95
+ return;
96
+ }
97
+
98
+ // Check if this is an AttrRef
99
+ if (value instanceof AttrRef) {
100
+ if (visited.has(value)) {
101
+ return;
102
+ }
103
+ visited.add(value);
104
+
105
+ const parent = value.parent.deref();
106
+ if (parent && isDeclarable(parent) && parent !== rootEntity) {
107
+ const parentName = entityToName.get(parent);
108
+ if (parentName) {
109
+ dependencies.add(parentName);
110
+ }
111
+ }
112
+ return;
113
+ }
114
+
115
+ // Check if this is a Declarable entity reference
116
+ if (isDeclarable(value)) {
117
+ // If this is the root entity itself, skip it but don't mark as visited
118
+ // so we can detect it if referenced again (self-reference)
119
+ if (value === rootEntity) {
120
+ if (visited.has(value)) {
121
+ // We've seen this self-reference before, record it as a dependency
122
+ const referencedName = entityToName.get(value);
123
+ if (referencedName) {
124
+ dependencies.add(referencedName);
125
+ }
126
+ return;
127
+ }
128
+ // First time seeing the root entity, mark as visited and continue scanning
129
+ visited.add(value);
130
+ scanProperties(value, entities, entityToName, dependencies, visited, rootEntity);
131
+ return;
132
+ }
133
+
134
+ // It's a different Declarable entity - record as dependency
135
+ const referencedName = entityToName.get(value);
136
+ if (referencedName) {
137
+ dependencies.add(referencedName);
138
+ }
139
+ // Don't recurse into other declarable entities
140
+ return;
141
+ }
142
+
143
+ // Check if we've already visited this object
144
+ if (visited.has(value)) {
145
+ return;
146
+ }
147
+
148
+ // Recurse into the object's properties
149
+ scanProperties(value, entities, entityToName, dependencies, visited, rootEntity);
150
+ }