@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,618 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import { LspServer } from "./server";
3
+ import { computeCapabilities } from "./capabilities";
4
+ import { toLspDiagnostics } from "./diagnostics";
5
+ import type { LexiconPlugin } from "../../lexicon";
6
+ import type { Serializer } from "../../serializer";
7
+ import type { CompletionContext, HoverContext, CodeActionContext, CompletionItem, HoverInfo, CodeAction } from "../../lsp/types";
8
+
9
+ function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
10
+ return {
11
+ name: "mock",
12
+ serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // capabilities
19
+ // ---------------------------------------------------------------------------
20
+
21
+ describe("computeCapabilities", () => {
22
+ test("always includes textDocumentSync and diagnosticProvider", () => {
23
+ const caps = computeCapabilities([]);
24
+ expect(caps.textDocumentSync).toBe(1);
25
+ expect(caps.diagnosticProvider).toBeDefined();
26
+ });
27
+
28
+ test("advertises completionProvider when any plugin provides it", () => {
29
+ const caps = computeCapabilities([
30
+ createMockPlugin({ completionProvider: () => [] }),
31
+ ]);
32
+ expect(caps.completionProvider).toBeDefined();
33
+ expect(caps.completionProvider!.triggerCharacters.length).toBeGreaterThan(0);
34
+ });
35
+
36
+ test("omits completionProvider when no plugin provides it", () => {
37
+ const caps = computeCapabilities([createMockPlugin()]);
38
+ expect(caps.completionProvider).toBeUndefined();
39
+ });
40
+
41
+ test("advertises hoverProvider when plugin provides it", () => {
42
+ const caps = computeCapabilities([
43
+ createMockPlugin({ hoverProvider: () => ({ contents: "hi" }) }),
44
+ ]);
45
+ expect(caps.hoverProvider).toBe(true);
46
+ });
47
+
48
+ test("omits hoverProvider when no plugin provides it", () => {
49
+ const caps = computeCapabilities([createMockPlugin()]);
50
+ expect(caps.hoverProvider).toBeUndefined();
51
+ });
52
+
53
+ test("advertises codeActionProvider when plugin provides it", () => {
54
+ const caps = computeCapabilities([
55
+ createMockPlugin({ codeActionProvider: () => [] }),
56
+ ]);
57
+ expect(caps.codeActionProvider).toBe(true);
58
+ });
59
+
60
+ test("omits codeActionProvider when no plugin provides it", () => {
61
+ const caps = computeCapabilities([createMockPlugin()]);
62
+ expect(caps.codeActionProvider).toBeUndefined();
63
+ });
64
+
65
+ test("combines capabilities from multiple plugins", () => {
66
+ const caps = computeCapabilities([
67
+ createMockPlugin({ completionProvider: () => [] }),
68
+ createMockPlugin({ hoverProvider: () => ({ contents: "x" }) }),
69
+ ]);
70
+ expect(caps.completionProvider).toBeDefined();
71
+ expect(caps.hoverProvider).toBe(true);
72
+ expect(caps.codeActionProvider).toBeUndefined();
73
+ });
74
+ });
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // diagnostics conversion
78
+ // ---------------------------------------------------------------------------
79
+
80
+ describe("toLspDiagnostics", () => {
81
+ test("converts lint diagnostic to LSP format (1-based to 0-based)", () => {
82
+ const result = toLspDiagnostics([
83
+ { file: "a.ts", line: 5, column: 3, ruleId: "R001", severity: "warning", message: "bad" },
84
+ ]);
85
+ expect(result).toHaveLength(1);
86
+ expect(result[0].range.start.line).toBe(4);
87
+ expect(result[0].range.start.character).toBe(2);
88
+ expect(result[0].severity).toBe(2);
89
+ expect(result[0].code).toBe("R001");
90
+ expect(result[0].source).toBe("chant");
91
+ expect(result[0].message).toBe("bad");
92
+ });
93
+
94
+ test("maps error severity to 1", () => {
95
+ const [d] = toLspDiagnostics([
96
+ { file: "a.ts", line: 1, column: 1, ruleId: "R", severity: "error", message: "" },
97
+ ]);
98
+ expect(d.severity).toBe(1);
99
+ });
100
+
101
+ test("maps info severity to 3", () => {
102
+ const [d] = toLspDiagnostics([
103
+ { file: "a.ts", line: 1, column: 1, ruleId: "R", severity: "info", message: "" },
104
+ ]);
105
+ expect(d.severity).toBe(3);
106
+ });
107
+
108
+ test("clamps negative line/column to 0", () => {
109
+ const [d] = toLspDiagnostics([
110
+ { file: "a.ts", line: 0, column: 0, ruleId: "R", severity: "error", message: "" },
111
+ ]);
112
+ expect(d.range.start.line).toBe(0);
113
+ expect(d.range.start.character).toBe(0);
114
+ });
115
+
116
+ test("includes fix data when fix is present", () => {
117
+ const fix = { range: [10, 20] as [number, number], replacement: "x" };
118
+ const [d] = toLspDiagnostics([
119
+ { file: "a.ts", line: 1, column: 1, ruleId: "R", severity: "error", message: "", fix },
120
+ ]);
121
+ expect(d.data?.fix).toBeDefined();
122
+ expect(d.data?.ruleId).toBe("R");
123
+ });
124
+
125
+ test("omits fix data when no fix", () => {
126
+ const [d] = toLspDiagnostics([
127
+ { file: "a.ts", line: 1, column: 1, ruleId: "R", severity: "error", message: "" },
128
+ ]);
129
+ expect(d.data?.fix).toBeUndefined();
130
+ expect(d.data?.ruleId).toBe("R");
131
+ });
132
+
133
+ test("converts multiple diagnostics", () => {
134
+ const result = toLspDiagnostics([
135
+ { file: "a.ts", line: 1, column: 1, ruleId: "A", severity: "error", message: "one" },
136
+ { file: "a.ts", line: 10, column: 5, ruleId: "B", severity: "warning", message: "two" },
137
+ { file: "b.ts", line: 3, column: 2, ruleId: "C", severity: "info", message: "three" },
138
+ ]);
139
+ expect(result).toHaveLength(3);
140
+ expect(result[0].code).toBe("A");
141
+ expect(result[1].range.start.line).toBe(9);
142
+ expect(result[2].severity).toBe(3);
143
+ });
144
+ });
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // LspServer — request dispatch
148
+ // ---------------------------------------------------------------------------
149
+
150
+ describe("LspServer", () => {
151
+ let server: LspServer;
152
+
153
+ describe("initialize", () => {
154
+ test("returns capabilities and serverInfo", async () => {
155
+ const plugin = createMockPlugin({ completionProvider: () => [] });
156
+ server = new LspServer([plugin], { captureNotifications: true });
157
+ const res = await server.sendRequest("initialize");
158
+
159
+ expect(res.error).toBeUndefined();
160
+ const result = res.result as Record<string, unknown>;
161
+ expect((result.serverInfo as Record<string, unknown>).name).toBe("chant");
162
+ const caps = result.capabilities as Record<string, unknown>;
163
+ expect(caps.textDocumentSync).toBe(1);
164
+ expect(caps.completionProvider).toBeDefined();
165
+ });
166
+
167
+ test("capabilities reflect loaded plugins", async () => {
168
+ server = new LspServer([], { captureNotifications: true });
169
+ const res = await server.sendRequest("initialize");
170
+ const caps = (res.result as Record<string, unknown>).capabilities as Record<string, unknown>;
171
+ expect(caps.completionProvider).toBeUndefined();
172
+ expect(caps.hoverProvider).toBeUndefined();
173
+ });
174
+ });
175
+
176
+ describe("shutdown", () => {
177
+ test("returns null", async () => {
178
+ server = new LspServer([], { captureNotifications: true });
179
+ const res = await server.sendRequest("shutdown");
180
+ expect(res.result).toBeNull();
181
+ expect(res.error).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ describe("unknown method", () => {
186
+ test("returns null for unknown method", async () => {
187
+ server = new LspServer([], { captureNotifications: true });
188
+ const res = await server.sendRequest("textDocument/unknown");
189
+ expect(res.result).toBeNull();
190
+ expect(res.error).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ // -----------------------------------------------------------------------
195
+ // Document synchronization
196
+ // -----------------------------------------------------------------------
197
+
198
+ describe("textDocument/didOpen", () => {
199
+ test("stores document content", () => {
200
+ server = new LspServer([], { captureNotifications: true });
201
+ server.handleNotification({
202
+ jsonrpc: "2.0",
203
+ method: "textDocument/didOpen",
204
+ params: { textDocument: { uri: "file:///a.ts", text: "const x = 1;" } },
205
+ });
206
+ expect(server.openDocuments.get("file:///a.ts")).toBe("const x = 1;");
207
+ });
208
+
209
+ test("emits publishDiagnostics notification", async () => {
210
+ server = new LspServer([], { captureNotifications: true });
211
+ server.handleNotification({
212
+ jsonrpc: "2.0",
213
+ method: "textDocument/didOpen",
214
+ params: { textDocument: { uri: "file:///a.ts", text: "const x = 1;" } },
215
+ });
216
+ // Give async diagnostics time to emit
217
+ await new Promise((r) => setTimeout(r, 50));
218
+ const diagNotif = server.sentNotifications.find(
219
+ (n) => n.method === "textDocument/publishDiagnostics",
220
+ );
221
+ expect(diagNotif).toBeDefined();
222
+ expect(diagNotif!.params.uri).toBe("file:///a.ts");
223
+ });
224
+ });
225
+
226
+ describe("textDocument/didChange", () => {
227
+ test("updates document content", () => {
228
+ server = new LspServer([], { captureNotifications: true });
229
+ server.handleNotification({
230
+ jsonrpc: "2.0",
231
+ method: "textDocument/didOpen",
232
+ params: { textDocument: { uri: "file:///a.ts", text: "old" } },
233
+ });
234
+ server.handleNotification({
235
+ jsonrpc: "2.0",
236
+ method: "textDocument/didChange",
237
+ params: {
238
+ textDocument: { uri: "file:///a.ts" },
239
+ contentChanges: [{ text: "new content" }],
240
+ },
241
+ });
242
+ expect(server.openDocuments.get("file:///a.ts")).toBe("new content");
243
+ });
244
+
245
+ test("uses last content change on full sync", () => {
246
+ server = new LspServer([], { captureNotifications: true });
247
+ server.handleNotification({
248
+ jsonrpc: "2.0",
249
+ method: "textDocument/didOpen",
250
+ params: { textDocument: { uri: "file:///a.ts", text: "orig" } },
251
+ });
252
+ server.handleNotification({
253
+ jsonrpc: "2.0",
254
+ method: "textDocument/didChange",
255
+ params: {
256
+ textDocument: { uri: "file:///a.ts" },
257
+ contentChanges: [{ text: "partial" }, { text: "final" }],
258
+ },
259
+ });
260
+ expect(server.openDocuments.get("file:///a.ts")).toBe("final");
261
+ });
262
+ });
263
+
264
+ describe("textDocument/didClose", () => {
265
+ test("removes document and clears diagnostics", async () => {
266
+ server = new LspServer([], { captureNotifications: true });
267
+ server.handleNotification({
268
+ jsonrpc: "2.0",
269
+ method: "textDocument/didOpen",
270
+ params: { textDocument: { uri: "file:///a.ts", text: "x" } },
271
+ });
272
+ expect(server.openDocuments.has("file:///a.ts")).toBe(true);
273
+
274
+ server.sentNotifications = [];
275
+ server.handleNotification({
276
+ jsonrpc: "2.0",
277
+ method: "textDocument/didClose",
278
+ params: { textDocument: { uri: "file:///a.ts" } },
279
+ });
280
+ expect(server.openDocuments.has("file:///a.ts")).toBe(false);
281
+
282
+ const clearNotif = server.sentNotifications.find(
283
+ (n) => n.method === "textDocument/publishDiagnostics",
284
+ );
285
+ expect(clearNotif).toBeDefined();
286
+ expect(clearNotif!.params.diagnostics).toEqual([]);
287
+ });
288
+ });
289
+
290
+ describe("initialized", () => {
291
+ test("handles initialized notification without error", () => {
292
+ server = new LspServer([], { captureNotifications: true });
293
+ // Should not throw
294
+ server.handleNotification({ jsonrpc: "2.0", method: "initialized" });
295
+ });
296
+ });
297
+
298
+ // -----------------------------------------------------------------------
299
+ // Completion
300
+ // -----------------------------------------------------------------------
301
+
302
+ describe("textDocument/completion", () => {
303
+ test("returns items from plugin completionProvider", async () => {
304
+ const items: CompletionItem[] = [
305
+ { label: "Bucket", kind: "resource", detail: "AWS::S3::Bucket" },
306
+ { label: "Table", kind: "resource", detail: "AWS::DynamoDB::Table" },
307
+ ];
308
+ const plugin = createMockPlugin({
309
+ completionProvider: () => items,
310
+ });
311
+ server = new LspServer([plugin], { captureNotifications: true });
312
+ server.openDocuments.set("file:///a.ts", "const b = new B");
313
+
314
+ const res = await server.sendRequest("textDocument/completion", {
315
+ textDocument: { uri: "file:///a.ts" },
316
+ position: { line: 0, character: 15 },
317
+ });
318
+
319
+ const result = res.result as { isIncomplete: boolean; items: CompletionItem[] };
320
+ expect(result.isIncomplete).toBe(false);
321
+ expect(result.items).toHaveLength(2);
322
+ expect(result.items[0].label).toBe("Bucket");
323
+ });
324
+
325
+ test("aggregates completions from multiple plugins", async () => {
326
+ const plugin1 = createMockPlugin({
327
+ name: "alpha",
328
+ completionProvider: () => [{ label: "AlphaItem" }],
329
+ });
330
+ const plugin2 = createMockPlugin({
331
+ name: "beta",
332
+ completionProvider: () => [{ label: "BetaItem" }],
333
+ });
334
+ server = new LspServer([plugin1, plugin2], { captureNotifications: true });
335
+ server.openDocuments.set("file:///a.ts", "x");
336
+
337
+ const res = await server.sendRequest("textDocument/completion", {
338
+ textDocument: { uri: "file:///a.ts" },
339
+ position: { line: 0, character: 1 },
340
+ });
341
+
342
+ const result = res.result as { items: CompletionItem[] };
343
+ expect(result.items).toHaveLength(2);
344
+ expect(result.items.map((i) => i.label)).toContain("AlphaItem");
345
+ expect(result.items.map((i) => i.label)).toContain("BetaItem");
346
+ });
347
+
348
+ test("returns empty items when no plugins have completionProvider", async () => {
349
+ server = new LspServer([createMockPlugin()], { captureNotifications: true });
350
+ server.openDocuments.set("file:///a.ts", "x");
351
+
352
+ const res = await server.sendRequest("textDocument/completion", {
353
+ textDocument: { uri: "file:///a.ts" },
354
+ position: { line: 0, character: 1 },
355
+ });
356
+
357
+ const result = res.result as { items: CompletionItem[] };
358
+ expect(result.items).toHaveLength(0);
359
+ });
360
+
361
+ test("extracts wordAtCursor and linePrefix correctly", async () => {
362
+ let capturedCtx: CompletionContext | undefined;
363
+ const plugin = createMockPlugin({
364
+ completionProvider: (ctx) => {
365
+ capturedCtx = ctx;
366
+ return [];
367
+ },
368
+ });
369
+ server = new LspServer([plugin], { captureNotifications: true });
370
+ server.openDocuments.set("file:///a.ts", "const bucket = new Buck");
371
+
372
+ await server.sendRequest("textDocument/completion", {
373
+ textDocument: { uri: "file:///a.ts" },
374
+ position: { line: 0, character: 23 },
375
+ });
376
+
377
+ expect(capturedCtx).toBeDefined();
378
+ expect(capturedCtx!.wordAtCursor).toBe("Buck");
379
+ expect(capturedCtx!.linePrefix).toBe("const bucket = new Buck");
380
+ });
381
+
382
+ test("handles document not in openDocuments gracefully", async () => {
383
+ const plugin = createMockPlugin({
384
+ completionProvider: () => [{ label: "Item" }],
385
+ });
386
+ server = new LspServer([plugin], { captureNotifications: true });
387
+ // Don't open any document — should use empty string
388
+
389
+ const res = await server.sendRequest("textDocument/completion", {
390
+ textDocument: { uri: "file:///unknown.ts" },
391
+ position: { line: 0, character: 0 },
392
+ });
393
+
394
+ expect(res.error).toBeUndefined();
395
+ const result = res.result as { items: CompletionItem[] };
396
+ expect(result.items).toHaveLength(1);
397
+ });
398
+ });
399
+
400
+ // -----------------------------------------------------------------------
401
+ // Hover
402
+ // -----------------------------------------------------------------------
403
+
404
+ describe("textDocument/hover", () => {
405
+ test("returns hover info from plugin", async () => {
406
+ const plugin = createMockPlugin({
407
+ hoverProvider: (ctx) => {
408
+ if (ctx.word === "Bucket") return { contents: "**Bucket** — S3 bucket" };
409
+ return undefined;
410
+ },
411
+ });
412
+ server = new LspServer([plugin], { captureNotifications: true });
413
+ server.openDocuments.set("file:///a.ts", "new Bucket()");
414
+
415
+ const res = await server.sendRequest("textDocument/hover", {
416
+ textDocument: { uri: "file:///a.ts" },
417
+ position: { line: 0, character: 6 },
418
+ });
419
+
420
+ const result = res.result as { contents: { kind: string; value: string } };
421
+ expect(result).not.toBeNull();
422
+ expect(result.contents.kind).toBe("markdown");
423
+ expect(result.contents.value).toContain("Bucket");
424
+ });
425
+
426
+ test("first plugin to return info wins", async () => {
427
+ const plugin1 = createMockPlugin({
428
+ name: "first",
429
+ hoverProvider: () => ({ contents: "from first" }),
430
+ });
431
+ const plugin2 = createMockPlugin({
432
+ name: "second",
433
+ hoverProvider: () => ({ contents: "from second" }),
434
+ });
435
+ server = new LspServer([plugin1, plugin2], { captureNotifications: true });
436
+ server.openDocuments.set("file:///a.ts", "word");
437
+
438
+ const res = await server.sendRequest("textDocument/hover", {
439
+ textDocument: { uri: "file:///a.ts" },
440
+ position: { line: 0, character: 2 },
441
+ });
442
+
443
+ const result = res.result as { contents: { value: string } };
444
+ expect(result.contents.value).toBe("from first");
445
+ });
446
+
447
+ test("returns null when no plugin returns info", async () => {
448
+ const plugin = createMockPlugin({
449
+ hoverProvider: () => undefined,
450
+ });
451
+ server = new LspServer([plugin], { captureNotifications: true });
452
+ server.openDocuments.set("file:///a.ts", "unknown");
453
+
454
+ const res = await server.sendRequest("textDocument/hover", {
455
+ textDocument: { uri: "file:///a.ts" },
456
+ position: { line: 0, character: 3 },
457
+ });
458
+
459
+ expect(res.result).toBeNull();
460
+ });
461
+
462
+ test("returns null when no plugins have hoverProvider", async () => {
463
+ server = new LspServer([createMockPlugin()], { captureNotifications: true });
464
+ server.openDocuments.set("file:///a.ts", "word");
465
+
466
+ const res = await server.sendRequest("textDocument/hover", {
467
+ textDocument: { uri: "file:///a.ts" },
468
+ position: { line: 0, character: 2 },
469
+ });
470
+
471
+ expect(res.result).toBeNull();
472
+ });
473
+
474
+ test("extracts word at position across word boundaries", async () => {
475
+ let capturedWord = "";
476
+ const plugin = createMockPlugin({
477
+ hoverProvider: (ctx) => {
478
+ capturedWord = ctx.word;
479
+ return undefined;
480
+ },
481
+ });
482
+ server = new LspServer([plugin], { captureNotifications: true });
483
+ server.openDocuments.set("file:///a.ts", "const myVar = 42;");
484
+
485
+ await server.sendRequest("textDocument/hover", {
486
+ textDocument: { uri: "file:///a.ts" },
487
+ position: { line: 0, character: 8 }, // middle of "myVar"
488
+ });
489
+
490
+ expect(capturedWord).toBe("myVar");
491
+ });
492
+ });
493
+
494
+ // -----------------------------------------------------------------------
495
+ // Code actions
496
+ // -----------------------------------------------------------------------
497
+
498
+ describe("textDocument/codeAction", () => {
499
+ test("returns actions from plugin", async () => {
500
+ const actions: CodeAction[] = [
501
+ { title: "Fix import", kind: "quickfix", isPreferred: true },
502
+ ];
503
+ const plugin = createMockPlugin({
504
+ codeActionProvider: () => actions,
505
+ });
506
+ server = new LspServer([plugin], { captureNotifications: true });
507
+ server.openDocuments.set("file:///a.ts", "code");
508
+
509
+ const res = await server.sendRequest("textDocument/codeAction", {
510
+ textDocument: { uri: "file:///a.ts" },
511
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } },
512
+ context: { diagnostics: [] },
513
+ });
514
+
515
+ const result = res.result as CodeAction[];
516
+ expect(result).toHaveLength(1);
517
+ expect(result[0].title).toBe("Fix import");
518
+ expect(result[0].kind).toBe("quickfix");
519
+ });
520
+
521
+ test("aggregates actions from multiple plugins", async () => {
522
+ const plugin1 = createMockPlugin({
523
+ codeActionProvider: () => [{ title: "Action A", kind: "quickfix" as const }],
524
+ });
525
+ const plugin2 = createMockPlugin({
526
+ codeActionProvider: () => [{ title: "Action B", kind: "refactor" as const }],
527
+ });
528
+ server = new LspServer([plugin1, plugin2], { captureNotifications: true });
529
+ server.openDocuments.set("file:///a.ts", "code");
530
+
531
+ const res = await server.sendRequest("textDocument/codeAction", {
532
+ textDocument: { uri: "file:///a.ts" },
533
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } },
534
+ context: { diagnostics: [] },
535
+ });
536
+
537
+ const result = res.result as CodeAction[];
538
+ expect(result).toHaveLength(2);
539
+ });
540
+
541
+ test("returns empty array when no plugins have codeActionProvider", async () => {
542
+ server = new LspServer([createMockPlugin()], { captureNotifications: true });
543
+ server.openDocuments.set("file:///a.ts", "code");
544
+
545
+ const res = await server.sendRequest("textDocument/codeAction", {
546
+ textDocument: { uri: "file:///a.ts" },
547
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } },
548
+ context: { diagnostics: [] },
549
+ });
550
+
551
+ const result = res.result as CodeAction[];
552
+ expect(result).toHaveLength(0);
553
+ });
554
+
555
+ test("passes diagnostic context to plugin", async () => {
556
+ let receivedCtx: CodeActionContext | undefined;
557
+ const plugin = createMockPlugin({
558
+ codeActionProvider: (ctx) => {
559
+ receivedCtx = ctx;
560
+ return [];
561
+ },
562
+ });
563
+ server = new LspServer([plugin], { captureNotifications: true });
564
+ server.openDocuments.set("file:///a.ts", "code");
565
+
566
+ await server.sendRequest("textDocument/codeAction", {
567
+ textDocument: { uri: "file:///a.ts" },
568
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } },
569
+ context: {
570
+ diagnostics: [
571
+ {
572
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } },
573
+ message: "test issue",
574
+ code: "R001",
575
+ severity: 1,
576
+ },
577
+ ],
578
+ },
579
+ });
580
+
581
+ expect(receivedCtx).toBeDefined();
582
+ expect(receivedCtx!.diagnostics).toHaveLength(1);
583
+ expect(receivedCtx!.diagnostics[0].ruleId).toBe("R001");
584
+ expect(receivedCtx!.diagnostics[0].severity).toBe("error");
585
+ });
586
+ });
587
+
588
+ // -----------------------------------------------------------------------
589
+ // Diagnostics (pull model)
590
+ // -----------------------------------------------------------------------
591
+
592
+ describe("textDocument/diagnostic", () => {
593
+ test("returns full diagnostic result", async () => {
594
+ server = new LspServer([], { captureNotifications: true });
595
+ server.openDocuments.set("file:///a.ts", "const x = 1;");
596
+
597
+ const res = await server.sendRequest("textDocument/diagnostic", {
598
+ textDocument: { uri: "file:///a.ts" },
599
+ });
600
+
601
+ const result = res.result as { kind: string; items: unknown[] };
602
+ expect(result.kind).toBe("full");
603
+ expect(Array.isArray(result.items)).toBe(true);
604
+ });
605
+
606
+ test("returns empty diagnostics for non-ts files", async () => {
607
+ server = new LspServer([], { captureNotifications: true });
608
+ server.openDocuments.set("file:///a.json", "{}");
609
+
610
+ const res = await server.sendRequest("textDocument/diagnostic", {
611
+ textDocument: { uri: "file:///a.json" },
612
+ });
613
+
614
+ const result = res.result as { items: unknown[] };
615
+ expect(result.items).toHaveLength(0);
616
+ });
617
+ });
618
+ });