@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,332 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { lintCommand, isLintRule, loadPluginRules, type LintOptions } from "./lint";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ describe("lintCommand", () => {
8
+ let testDir: string;
9
+
10
+ beforeEach(async () => {
11
+ testDir = join(tmpdir(), `chant-lint-test-${Date.now()}-${Math.random()}`);
12
+ await mkdir(testDir, { recursive: true });
13
+ process.env.NO_COLOR = "1";
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await rm(testDir, { recursive: true, force: true });
18
+ delete process.env.NO_COLOR;
19
+ });
20
+
21
+ test("returns success for empty directory", async () => {
22
+ const options: LintOptions = {
23
+ path: testDir,
24
+ format: "stylish",
25
+ };
26
+
27
+ const result = await lintCommand(options);
28
+
29
+ expect(result.success).toBe(true);
30
+ expect(result.errorCount).toBe(0);
31
+ expect(result.warningCount).toBe(0);
32
+ });
33
+
34
+ test("returns success for clean file", async () => {
35
+ await writeFile(
36
+ join(testDir, "clean.ts"),
37
+ `
38
+ export const config = { a: 1 };
39
+ `
40
+ );
41
+
42
+ const options: LintOptions = {
43
+ path: testDir,
44
+ format: "stylish",
45
+ };
46
+
47
+ const result = await lintCommand(options);
48
+
49
+ expect(result.success).toBe(true);
50
+ expect(result.errorCount).toBe(0);
51
+ });
52
+
53
+ test("detects flat-declarations violations", async () => {
54
+ await writeFile(
55
+ join(testDir, "nested.ts"),
56
+ `
57
+ class Bucket {}
58
+ export const b = new Bucket({ encryption: { algo: "AES256" } });
59
+ `
60
+ );
61
+
62
+ const options: LintOptions = {
63
+ path: testDir,
64
+ format: "stylish",
65
+ };
66
+
67
+ const result = await lintCommand(options);
68
+ expect(result.diagnostics.some((d) => d.ruleId === "COR001")).toBe(true);
69
+ });
70
+
71
+ test("formats output as JSON", async () => {
72
+ await writeFile(
73
+ join(testDir, "nested.ts"),
74
+ `
75
+ export const config = { a: { b: { c: { d: 1 } } } };
76
+ `
77
+ );
78
+
79
+ const options: LintOptions = {
80
+ path: testDir,
81
+ format: "json",
82
+ };
83
+
84
+ const result = await lintCommand(options);
85
+
86
+ expect(() => JSON.parse(result.output)).not.toThrow();
87
+ });
88
+
89
+ test("formats output as SARIF", async () => {
90
+ await writeFile(
91
+ join(testDir, "nested.ts"),
92
+ `
93
+ export const config = { a: { b: { c: { d: 1 } } } };
94
+ `
95
+ );
96
+
97
+ const options: LintOptions = {
98
+ path: testDir,
99
+ format: "sarif",
100
+ };
101
+
102
+ const result = await lintCommand(options);
103
+
104
+ expect(() => JSON.parse(result.output)).not.toThrow();
105
+ const sarif = JSON.parse(result.output);
106
+ expect(sarif.version).toBe("2.1.0");
107
+ });
108
+
109
+ test("excludes test files from linting", async () => {
110
+ await writeFile(
111
+ join(testDir, "app.test.ts"),
112
+ `
113
+ export const config = { a: { b: { c: { d: 1 } } } };
114
+ `
115
+ );
116
+
117
+ const options: LintOptions = {
118
+ path: testDir,
119
+ format: "stylish",
120
+ };
121
+
122
+ const result = await lintCommand(options);
123
+
124
+ // Test files should be excluded
125
+ expect(result.diagnostics).toHaveLength(0);
126
+ });
127
+
128
+ test("excludes node_modules", async () => {
129
+ const nodeModulesDir = join(testDir, "node_modules", "some-pkg");
130
+ await mkdir(nodeModulesDir, { recursive: true });
131
+ await writeFile(
132
+ join(nodeModulesDir, "index.ts"),
133
+ `
134
+ export const config = { a: { b: { c: { d: 1 } } } };
135
+ `
136
+ );
137
+
138
+ const options: LintOptions = {
139
+ path: testDir,
140
+ format: "stylish",
141
+ };
142
+
143
+ const result = await lintCommand(options);
144
+
145
+ // node_modules should be excluded
146
+ expect(result.diagnostics).toHaveLength(0);
147
+ });
148
+
149
+ test("counts errors and warnings separately", async () => {
150
+ await writeFile(
151
+ join(testDir, "nested.ts"),
152
+ `
153
+ class Bucket {}
154
+ export const a = new Bucket({ config: { x: 1 } });
155
+ export const b = new Bucket({ config: { y: 2 } });
156
+ `
157
+ );
158
+
159
+ const options: LintOptions = {
160
+ path: testDir,
161
+ format: "stylish",
162
+ };
163
+
164
+ const result = await lintCommand(options);
165
+
166
+ // COR001 fires on inline objects — strict preset sets it to "error"
167
+ expect(result.diagnostics.some(d => d.ruleId === "COR001")).toBe(true);
168
+ expect(result.diagnostics.filter(d => d.ruleId === "COR001")).toHaveLength(2);
169
+ });
170
+ });
171
+
172
+ describe("isLintRule", () => {
173
+ test("returns true for valid lint rule objects", () => {
174
+ expect(
175
+ isLintRule({
176
+ id: "TEST001",
177
+ severity: "warning",
178
+ category: "style",
179
+ check() {
180
+ return [];
181
+ },
182
+ }),
183
+ ).toBe(true);
184
+ });
185
+
186
+ test("returns false for non-rule objects", () => {
187
+ expect(isLintRule({ foo: "bar" })).toBe(false);
188
+ expect(isLintRule(null)).toBe(false);
189
+ expect(isLintRule(undefined)).toBe(false);
190
+ expect(isLintRule("string")).toBe(false);
191
+ expect(isLintRule(42)).toBe(false);
192
+ });
193
+
194
+ test("returns false when check is not a function", () => {
195
+ expect(
196
+ isLintRule({
197
+ id: "TEST001",
198
+ severity: "warning",
199
+ category: "style",
200
+ check: "not-a-function",
201
+ }),
202
+ ).toBe(false);
203
+ });
204
+
205
+ test("returns false when id is not a string", () => {
206
+ expect(
207
+ isLintRule({
208
+ id: 123,
209
+ severity: "warning",
210
+ category: "style",
211
+ check() {
212
+ return [];
213
+ },
214
+ }),
215
+ ).toBe(false);
216
+ });
217
+ });
218
+
219
+ describe("loadPluginRules", () => {
220
+ test("loads rules from a plugin file", async () => {
221
+ const fixtureDir = resolve(import.meta.dir, "__fixtures__");
222
+ const rules = await loadPluginRules(["./sample-rule.ts"], fixtureDir);
223
+
224
+ expect(rules.size).toBe(1);
225
+ expect(rules.has("TEST001")).toBe(true);
226
+
227
+ const rule = rules.get("TEST001")!;
228
+ expect(rule.severity).toBe("warning");
229
+ expect(rule.category).toBe("style");
230
+ });
231
+
232
+ test("silently skips non-LintRule exports", async () => {
233
+ const fixtureDir = resolve(import.meta.dir, "__fixtures__");
234
+ const rules = await loadPluginRules(["./sample-rule.ts"], fixtureDir);
235
+
236
+ // sample-rule.ts exports notARule too, which should be skipped
237
+ expect(rules.size).toBe(1);
238
+ });
239
+
240
+ test("throws meaningful error for invalid plugin path", async () => {
241
+ await expect(loadPluginRules(["./nonexistent.ts"], "/tmp")).rejects.toThrow(
242
+ /Failed to load plugin "\.\/nonexistent\.ts"/,
243
+ );
244
+ });
245
+ });
246
+
247
+ describe("plugin integration", () => {
248
+ let testDir: string;
249
+
250
+ beforeEach(async () => {
251
+ testDir = join(tmpdir(), `chant-plugin-test-${Date.now()}-${Math.random()}`);
252
+ await mkdir(testDir, { recursive: true });
253
+ process.env.NO_COLOR = "1";
254
+ });
255
+
256
+ afterEach(async () => {
257
+ await rm(testDir, { recursive: true, force: true });
258
+ delete process.env.NO_COLOR;
259
+ });
260
+
261
+ test("plugin rule is loaded via config and available during lint", async () => {
262
+ // Put plugin in a dot-directory so it's not scanned as a source file
263
+ const pluginDir = join(testDir, ".plugins");
264
+ await mkdir(pluginDir, { recursive: true });
265
+
266
+ await writeFile(
267
+ join(pluginDir, "my-rule.ts"),
268
+ `export const myRule = {
269
+ id: "PLUG001",
270
+ severity: "warning",
271
+ category: "style",
272
+ check() { return []; },
273
+ };`,
274
+ );
275
+
276
+ // Write config that references the plugin
277
+ await writeFile(
278
+ join(testDir, "chant.config.json"),
279
+ JSON.stringify({ plugins: ["./.plugins/my-rule.ts"] }),
280
+ );
281
+
282
+ // Write a clean TS file so lint runs
283
+ await writeFile(join(testDir, "index.ts"), `export const x = 1;\n`);
284
+
285
+ const result = await lintCommand({ path: testDir, format: "stylish" });
286
+ // Plugin rule returns no diagnostics, so lint should pass
287
+ expect(result.success).toBe(true);
288
+ });
289
+
290
+ test("plugin rule respects config severity override", async () => {
291
+ // Put plugin in a dot-directory so it's not scanned as a source file
292
+ const pluginDir = join(testDir, ".plugins");
293
+ await mkdir(pluginDir, { recursive: true });
294
+
295
+ // Write a plugin rule that always produces a warning diagnostic
296
+ await writeFile(
297
+ join(pluginDir, "warn-rule.ts"),
298
+ `export const warnRule = {
299
+ id: "PLUG002",
300
+ severity: "warning",
301
+ category: "correctness",
302
+ check(ctx) {
303
+ return [{
304
+ file: ctx.filePath,
305
+ line: 1,
306
+ column: 1,
307
+ ruleId: "PLUG002",
308
+ severity: "warning",
309
+ message: "Plugin warning",
310
+ }];
311
+ },
312
+ };`,
313
+ );
314
+
315
+ // Config overrides the severity to "error"
316
+ await writeFile(
317
+ join(testDir, "chant.config.json"),
318
+ JSON.stringify({
319
+ plugins: ["./.plugins/warn-rule.ts"],
320
+ rules: { PLUG002: "error" },
321
+ }),
322
+ );
323
+
324
+ await writeFile(join(testDir, "index.ts"), `export const x = 1;\n`);
325
+
326
+ const result = await lintCommand({ path: testDir, format: "stylish" });
327
+
328
+ const pluginDiags = result.diagnostics.filter((d) => d.ruleId === "PLUG002");
329
+ // Plugin rule should produce exactly one diagnostic (one source file)
330
+ expect(pluginDiags.length).toBe(1);
331
+ });
332
+ });