@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,172 @@
1
+ import type { LintRule, LintDiagnostic, LintContext, LintRunOptions } from "./rule";
2
+ import { parseFile } from "./parser";
3
+ import { readFileSync } from "fs";
4
+
5
+ /**
6
+ * Represents a disable directive found in source code comments
7
+ */
8
+ interface DisableDirective {
9
+ /** Line number where the directive appears (1-based) */
10
+ line: number;
11
+ /** Type of directive */
12
+ type: "file" | "line" | "next-line";
13
+ /** Specific rule IDs to disable, or undefined for all rules */
14
+ ruleIds?: string[];
15
+ }
16
+
17
+ /**
18
+ * Parse disable comments from source code
19
+ * Supports:
20
+ * - // chant-disable - disable all rules for entire file
21
+ * - // chant-disable-line - disable all rules for current line
22
+ * - // chant-disable-next-line - disable all rules for next line
23
+ * - // chant-disable rule-id1 rule-id2 - disable specific rules for file
24
+ * - // chant-disable-line rule-id1 - disable specific rules for line
25
+ * - // chant-disable-next-line rule-id1 - disable specific rules for next line
26
+ */
27
+ function parseDisableComments(content: string): DisableDirective[] {
28
+ const directives: DisableDirective[] = [];
29
+ const lines = content.split("\n");
30
+
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ const lineNumber = i + 1;
34
+
35
+ // Match disable comments
36
+ const disableFileMatch = line.match(/\/\/\s*chant-disable(?:\s+(.+))?$/);
37
+ const disableLineMatch = line.match(/\/\/\s*chant-disable-line(?:\s+(.+))?$/);
38
+ const disableNextLineMatch = line.match(/\/\/\s*chant-disable-next-line(?:\s+(.+))?$/);
39
+
40
+ if (disableNextLineMatch) {
41
+ const ruleIds = disableNextLineMatch[1]?.trim().split(/\s+/).filter(Boolean);
42
+ directives.push({
43
+ line: lineNumber + 1, // Next line
44
+ type: "next-line",
45
+ ruleIds: ruleIds && ruleIds.length > 0 ? ruleIds : undefined,
46
+ });
47
+ } else if (disableLineMatch) {
48
+ const ruleIds = disableLineMatch[1]?.trim().split(/\s+/).filter(Boolean);
49
+ directives.push({
50
+ line: lineNumber,
51
+ type: "line",
52
+ ruleIds: ruleIds && ruleIds.length > 0 ? ruleIds : undefined,
53
+ });
54
+ } else if (disableFileMatch) {
55
+ const ruleIds = disableFileMatch[1]?.trim().split(/\s+/).filter(Boolean);
56
+ directives.push({
57
+ line: lineNumber,
58
+ type: "file",
59
+ ruleIds: ruleIds && ruleIds.length > 0 ? ruleIds : undefined,
60
+ });
61
+ }
62
+ }
63
+
64
+ return directives;
65
+ }
66
+
67
+ /**
68
+ * Check if a diagnostic should be suppressed based on disable directives
69
+ */
70
+ function isDiagnosticDisabled(
71
+ diagnostic: LintDiagnostic,
72
+ directives: DisableDirective[],
73
+ allRuleIds: Set<string>
74
+ ): boolean {
75
+ // Check for file-level disables
76
+ const fileDisables = directives.filter((d) => d.type === "file");
77
+ for (const directive of fileDisables) {
78
+ if (!directive.ruleIds) {
79
+ // Disable all rules
80
+ return true;
81
+ }
82
+ // Check if rule ID exists before checking if it's disabled
83
+ if (directive.ruleIds.some((id) => allRuleIds.has(id))) {
84
+ if (directive.ruleIds.includes(diagnostic.ruleId)) {
85
+ return true;
86
+ }
87
+ }
88
+ // Silently ignore non-existent rule IDs
89
+ }
90
+
91
+ // Check for line-specific disables
92
+ const lineDisables = directives.filter(
93
+ (d) => (d.type === "line" || d.type === "next-line") && d.line === diagnostic.line
94
+ );
95
+ for (const directive of lineDisables) {
96
+ if (!directive.ruleIds) {
97
+ // Disable all rules
98
+ return true;
99
+ }
100
+ // Check if rule ID exists before checking if it's disabled
101
+ if (directive.ruleIds.some((id) => allRuleIds.has(id))) {
102
+ if (directive.ruleIds.includes(diagnostic.ruleId)) {
103
+ return true;
104
+ }
105
+ }
106
+ // Silently ignore non-existent rule IDs
107
+ }
108
+
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Execute lint rules on a set of files
114
+ * @param files - Array of file paths to lint
115
+ * @param rules - Array of lint rules to execute
116
+ * @param ruleOptions - Optional map of rule ID to options object
117
+ * @returns Array of diagnostics from all rules and files, with disable comments applied
118
+ */
119
+ export async function runLint(
120
+ files: string[],
121
+ rules: LintRule[],
122
+ ruleOptions?: Map<string, Record<string, unknown>>,
123
+ runOptions?: LintRunOptions,
124
+ ): Promise<LintDiagnostic[]> {
125
+ const allDiagnostics: LintDiagnostic[] = [];
126
+ const allRuleIds = new Set(rules.map((r) => r.id));
127
+
128
+ for (const filePath of files) {
129
+ try {
130
+ // Parse the file
131
+ const sourceFile = parseFile(filePath);
132
+
133
+ // Read file content for disable comment parsing
134
+ const content = readFileSync(filePath, "utf-8");
135
+ const directives = parseDisableComments(content);
136
+
137
+ // Create lint context
138
+ const context: LintContext = {
139
+ sourceFile,
140
+ entities: [],
141
+ filePath,
142
+ lexicon: undefined,
143
+ barrelExports: runOptions?.barrelExports,
144
+ projectExports: runOptions?.projectExports,
145
+ projectScan: runOptions?.projectScan,
146
+ };
147
+
148
+ // Execute each rule
149
+ for (const rule of rules) {
150
+ const options = ruleOptions?.get(rule.id);
151
+ const diagnostics = rule.check(context, options);
152
+ allDiagnostics.push(...diagnostics);
153
+ }
154
+
155
+ // Filter out disabled diagnostics for this file
156
+ const filteredDiagnostics = allDiagnostics.filter(
157
+ (diagnostic) => diagnostic.file === filePath && !isDiagnosticDisabled(diagnostic, directives, allRuleIds)
158
+ );
159
+
160
+ // Replace file diagnostics with filtered ones
161
+ const otherFileDiagnostics = allDiagnostics.filter((d) => d.file !== filePath);
162
+ allDiagnostics.length = 0;
163
+ allDiagnostics.push(...otherFileDiagnostics, ...filteredDiagnostics);
164
+ } catch (error) {
165
+ // If parsing fails, skip this file and continue with others
166
+ // In a real implementation, you might want to collect these errors
167
+ continue;
168
+ }
169
+ }
170
+
171
+ return allDiagnostics;
172
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import { registerCheck, getNamedCheck, listChecks } from "./named-checks";
4
+ import type { LintContext } from "./rule";
5
+
6
+ describe("named-checks", () => {
7
+ test("registerCheck and getNamedCheck", () => {
8
+ registerCheck("is-string-literal", (node) => ts.isStringLiteral(node));
9
+ const check = getNamedCheck("is-string-literal");
10
+ expect(check).toBeDefined();
11
+
12
+ const sf = ts.createSourceFile("test.ts", `const a = "hello";`, ts.ScriptTarget.Latest, true);
13
+ const ctx: LintContext = { sourceFile: sf, entities: [], filePath: "test.ts" };
14
+
15
+ // Test with a string literal node
16
+ let found = false;
17
+ ts.forEachChild(sf, function visit(node) {
18
+ if (ts.isStringLiteral(node)) {
19
+ found = check!(node, ctx);
20
+ }
21
+ ts.forEachChild(node, visit);
22
+ });
23
+ expect(found).toBe(true);
24
+ });
25
+
26
+ test("getNamedCheck returns undefined for unknown check", () => {
27
+ expect(getNamedCheck("nonexistent-check")).toBeUndefined();
28
+ });
29
+
30
+ test("listChecks returns registered check names", () => {
31
+ registerCheck("test-check-a", () => true);
32
+ registerCheck("test-check-b", () => false);
33
+ const checks = listChecks();
34
+ expect(checks).toContain("test-check-a");
35
+ expect(checks).toContain("test-check-b");
36
+ });
37
+ });
@@ -0,0 +1,33 @@
1
+ import type * as ts from "typescript";
2
+ import type { LintContext } from "./rule";
3
+
4
+ /**
5
+ * A named check function that evaluates a condition on a node.
6
+ */
7
+ export type NamedCheckFn = (node: ts.Node, context: LintContext) => boolean;
8
+
9
+ /**
10
+ * Registry of named check functions.
11
+ */
12
+ const checkRegistry = new Map<string, NamedCheckFn>();
13
+
14
+ /**
15
+ * Register a named check function.
16
+ */
17
+ export function registerCheck(name: string, fn: NamedCheckFn): void {
18
+ checkRegistry.set(name, fn);
19
+ }
20
+
21
+ /**
22
+ * Get a named check by name. Returns undefined if not found.
23
+ */
24
+ export function getNamedCheck(name: string): NamedCheckFn | undefined {
25
+ return checkRegistry.get(name);
26
+ }
27
+
28
+ /**
29
+ * List all registered check names.
30
+ */
31
+ export function listChecks(): string[] {
32
+ return Array.from(checkRegistry.keys());
33
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { parseFile } from "./parser";
3
+ import { writeFileSync, unlinkSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ import * as ts from "typescript";
6
+
7
+ const TEST_DIR = join(import.meta.dir, "__test_parser__");
8
+ const VALID_FILE = join(TEST_DIR, "valid.ts");
9
+ const INVALID_FILE = join(TEST_DIR, "invalid.ts");
10
+ const NONEXISTENT_FILE = join(TEST_DIR, "nonexistent.ts");
11
+
12
+ beforeAll(() => {
13
+ mkdirSync(TEST_DIR, { recursive: true });
14
+
15
+ // Create a valid TypeScript file
16
+ writeFileSync(
17
+ VALID_FILE,
18
+ `export interface User {
19
+ name: string;
20
+ age: number;
21
+ }
22
+
23
+ export function greet(user: User): string {
24
+ return \`Hello, \${user.name}!\`;
25
+ }
26
+ `
27
+ );
28
+
29
+ // Create an invalid TypeScript file (syntax error)
30
+ writeFileSync(
31
+ INVALID_FILE,
32
+ `export interface User {
33
+ name: string
34
+ age: number // missing semicolon is ok, but let's make a real syntax error
35
+ }
36
+
37
+ export function broken( {
38
+ return "missing closing paren and body";
39
+ `
40
+ );
41
+ });
42
+
43
+ afterAll(() => {
44
+ try {
45
+ unlinkSync(VALID_FILE);
46
+ } catch {}
47
+ try {
48
+ unlinkSync(INVALID_FILE);
49
+ } catch {}
50
+ try {
51
+ unlinkSync(NONEXISTENT_FILE);
52
+ } catch {}
53
+ });
54
+
55
+ describe("parseFile", () => {
56
+ test("parses valid TypeScript file and returns SourceFile", () => {
57
+ const sourceFile = parseFile(VALID_FILE);
58
+
59
+ expect(sourceFile).toBeDefined();
60
+ expect(sourceFile.fileName).toBe(VALID_FILE);
61
+ expect(sourceFile.kind).toBe(ts.SyntaxKind.SourceFile);
62
+ });
63
+
64
+ test("returns SourceFile with expected structure", () => {
65
+ const sourceFile = parseFile(VALID_FILE);
66
+
67
+ // Should have statements (interface and function declarations)
68
+ expect(sourceFile.statements.length).toBeGreaterThan(0);
69
+
70
+ // First statement should be interface declaration
71
+ const firstStatement = sourceFile.statements[0];
72
+ expect(firstStatement.kind).toBe(ts.SyntaxKind.InterfaceDeclaration);
73
+
74
+ // Second statement should be function declaration
75
+ const secondStatement = sourceFile.statements[1];
76
+ expect(secondStatement.kind).toBe(ts.SyntaxKind.FunctionDeclaration);
77
+ });
78
+
79
+ test("sets parent nodes correctly", () => {
80
+ const sourceFile = parseFile(VALID_FILE);
81
+
82
+ // Check that parent nodes are set
83
+ const firstStatement = sourceFile.statements[0];
84
+ expect(firstStatement.parent).toBe(sourceFile);
85
+ });
86
+
87
+ test("throws clear error for invalid TypeScript", () => {
88
+ expect(() => parseFile(INVALID_FILE)).toThrow("TypeScript parsing failed");
89
+ });
90
+
91
+ test("throws error with file location for syntax errors", () => {
92
+ try {
93
+ parseFile(INVALID_FILE);
94
+ expect.unreachable("Should have thrown an error");
95
+ } catch (err) {
96
+ expect(err).toBeInstanceOf(Error);
97
+ const error = err as Error;
98
+ expect(error.message).toContain(INVALID_FILE);
99
+ expect(error.message).toContain("TypeScript parsing failed");
100
+ }
101
+ });
102
+
103
+ test("throws clear error when file does not exist", () => {
104
+ expect(() => parseFile(NONEXISTENT_FILE)).toThrow("Failed to read file");
105
+ expect(() => parseFile(NONEXISTENT_FILE)).toThrow(NONEXISTENT_FILE);
106
+ });
107
+
108
+ test("handles empty file", () => {
109
+ const emptyFile = join(TEST_DIR, "empty.ts");
110
+ writeFileSync(emptyFile, "");
111
+
112
+ const sourceFile = parseFile(emptyFile);
113
+ expect(sourceFile).toBeDefined();
114
+ expect(sourceFile.statements.length).toBe(0);
115
+
116
+ unlinkSync(emptyFile);
117
+ });
118
+
119
+ test("handles file with only comments", () => {
120
+ const commentsFile = join(TEST_DIR, "comments.ts");
121
+ writeFileSync(commentsFile, "// Just a comment\n/* Another comment */");
122
+
123
+ const sourceFile = parseFile(commentsFile);
124
+ expect(sourceFile).toBeDefined();
125
+ expect(sourceFile.statements.length).toBe(0);
126
+
127
+ unlinkSync(commentsFile);
128
+ });
129
+ });
@@ -0,0 +1,42 @@
1
+ import * as ts from "typescript";
2
+ import { readFileSync } from "fs";
3
+
4
+ /**
5
+ * Parse a TypeScript file into an AST
6
+ * @param path - Absolute or relative path to the TypeScript file
7
+ * @returns Parsed TypeScript SourceFile
8
+ * @throws Error if the file cannot be read or parsed
9
+ */
10
+ export function parseFile(path: string): ts.SourceFile {
11
+ let content: string;
12
+
13
+ try {
14
+ content = readFileSync(path, "utf-8");
15
+ } catch (err) {
16
+ throw new Error(`Failed to read file ${path}: ${err instanceof Error ? err.message : String(err)}`);
17
+ }
18
+
19
+ const sourceFile = ts.createSourceFile(
20
+ path,
21
+ content,
22
+ ts.ScriptTarget.Latest,
23
+ true // setParentNodes
24
+ );
25
+
26
+ // Check for syntax errors (parseDiagnostics is internal but accessible)
27
+ const diagnostics = [
28
+ ...(sourceFile as unknown as { parseDiagnostics: ts.DiagnosticWithLocation[] }).parseDiagnostics,
29
+ ];
30
+
31
+ if (diagnostics.length > 0) {
32
+ const errors = diagnostics
33
+ .map((d) => {
34
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(d.start ?? 0);
35
+ return `${path}:${line + 1}:${character + 1} - ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
36
+ })
37
+ .join("\n");
38
+ throw new Error(`TypeScript parsing failed:\n${errors}`);
39
+ }
40
+
41
+ return sourceFile;
42
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { runPostSynthChecks } from "./post-synth";
3
+ import type { PostSynthCheck, PostSynthContext } from "./post-synth";
4
+
5
+ function createBuildResult(overrides: Partial<PostSynthContext["buildResult"]> = {}) {
6
+ return {
7
+ outputs: new Map<string, string>(),
8
+ entities: new Map(),
9
+ warnings: [] as string[],
10
+ errors: [] as Array<{ message: string; name: string }>,
11
+ sourceFileCount: 0,
12
+ ...overrides,
13
+ };
14
+ }
15
+
16
+ describe("post-synth checks", () => {
17
+ test("runs check and collects diagnostics", () => {
18
+ const check: PostSynthCheck = {
19
+ id: "PS001",
20
+ description: "Check for empty outputs",
21
+ check(ctx) {
22
+ if (ctx.outputs.size === 0) {
23
+ return [{ checkId: "PS001", severity: "warning", message: "No outputs produced" }];
24
+ }
25
+ return [];
26
+ },
27
+ };
28
+
29
+ const result = createBuildResult();
30
+ const diags = runPostSynthChecks([check], result);
31
+ expect(diags).toHaveLength(1);
32
+ expect(diags[0].checkId).toBe("PS001");
33
+ expect(diags[0].severity).toBe("warning");
34
+ expect(diags[0].message).toBe("No outputs produced");
35
+ });
36
+
37
+ test("returns empty when no issues found", () => {
38
+ const check: PostSynthCheck = {
39
+ id: "PS002",
40
+ description: "Always passes",
41
+ check() {
42
+ return [];
43
+ },
44
+ };
45
+
46
+ const diags = runPostSynthChecks([check], createBuildResult());
47
+ expect(diags).toHaveLength(0);
48
+ });
49
+
50
+ test("aggregates diagnostics from multiple checks", () => {
51
+ const checks: PostSynthCheck[] = [
52
+ {
53
+ id: "PS003A",
54
+ description: "Check A",
55
+ check() {
56
+ return [{ checkId: "PS003A", severity: "error", message: "Error A" }];
57
+ },
58
+ },
59
+ {
60
+ id: "PS003B",
61
+ description: "Check B",
62
+ check() {
63
+ return [
64
+ { checkId: "PS003B", severity: "warning", message: "Warning B1" },
65
+ { checkId: "PS003B", severity: "info", message: "Info B2" },
66
+ ];
67
+ },
68
+ },
69
+ ];
70
+
71
+ const diags = runPostSynthChecks(checks, createBuildResult());
72
+ expect(diags).toHaveLength(3);
73
+ expect(diags[0].checkId).toBe("PS003A");
74
+ expect(diags[1].checkId).toBe("PS003B");
75
+ expect(diags[2].checkId).toBe("PS003B");
76
+ });
77
+
78
+ test("provides entities and outputs in context", () => {
79
+ const entities = new Map([["myBucket", { kind: "resource" }]]);
80
+ const outputs = new Map([["aws", '{"AWSTemplateFormatVersion":"2010-09-09"}']]);
81
+
82
+ const check: PostSynthCheck = {
83
+ id: "PS004",
84
+ description: "Check entities",
85
+ check(ctx) {
86
+ const diags = [];
87
+ for (const [name] of ctx.entities) {
88
+ diags.push({
89
+ checkId: "PS004",
90
+ severity: "info" as const,
91
+ message: `Found entity: ${name}`,
92
+ entity: name,
93
+ lexicon: "aws",
94
+ });
95
+ }
96
+ return diags;
97
+ },
98
+ };
99
+
100
+ const diags = runPostSynthChecks(
101
+ [check],
102
+ createBuildResult({ entities: entities as never, outputs }),
103
+ );
104
+ expect(diags).toHaveLength(1);
105
+ expect(diags[0].entity).toBe("myBucket");
106
+ expect(diags[0].lexicon).toBe("aws");
107
+ });
108
+
109
+ test("handles empty check list", () => {
110
+ const diags = runPostSynthChecks([], createBuildResult());
111
+ expect(diags).toHaveLength(0);
112
+ });
113
+ });
@@ -0,0 +1,76 @@
1
+ import type { Declarable } from "../declarable";
2
+ import type { SerializerResult } from "../serializer";
3
+ import type { Severity } from "./rule";
4
+
5
+ /**
6
+ * Context provided to post-synthesis checks.
7
+ */
8
+ export interface PostSynthContext {
9
+ /** The build result outputs (lexicon name → serialized output) */
10
+ outputs: Map<string, string | SerializerResult>;
11
+ /** Map of entity name to Declarable entity */
12
+ entities: Map<string, Declarable>;
13
+ /** Raw build result object */
14
+ buildResult: {
15
+ outputs: Map<string, string | SerializerResult>;
16
+ entities: Map<string, Declarable>;
17
+ warnings: string[];
18
+ errors: Array<{ message: string; name: string }>;
19
+ sourceFileCount: number;
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Extract the primary content string from a serializer output.
25
+ */
26
+ export function getPrimaryOutput(output: string | SerializerResult): string {
27
+ return typeof output === "string" ? output : output.primary;
28
+ }
29
+
30
+ /**
31
+ * A diagnostic from a post-synthesis check.
32
+ */
33
+ export interface PostSynthDiagnostic {
34
+ /** ID of the check that produced this diagnostic */
35
+ checkId: string;
36
+ /** Severity level */
37
+ severity: Severity;
38
+ /** Human-readable message */
39
+ message: string;
40
+ /** Optional entity name related to this diagnostic */
41
+ entity?: string;
42
+ /** Optional lexicon related to this diagnostic */
43
+ lexicon?: string;
44
+ }
45
+
46
+ /**
47
+ * A post-synthesis check that validates build output.
48
+ */
49
+ export interface PostSynthCheck {
50
+ /** Unique identifier for this check */
51
+ id: string;
52
+ /** Human-readable description */
53
+ description: string;
54
+ /** Execute the check and return diagnostics */
55
+ check(ctx: PostSynthContext): PostSynthDiagnostic[];
56
+ }
57
+
58
+ /**
59
+ * Run a set of post-synthesis checks against a build result.
60
+ */
61
+ export function runPostSynthChecks(
62
+ checks: PostSynthCheck[],
63
+ buildResult: PostSynthContext["buildResult"],
64
+ ): PostSynthDiagnostic[] {
65
+ const ctx: PostSynthContext = {
66
+ outputs: buildResult.outputs,
67
+ entities: buildResult.entities,
68
+ buildResult,
69
+ };
70
+
71
+ const diagnostics: PostSynthDiagnostic[] = [];
72
+ for (const check of checks) {
73
+ diagnostics.push(...check.check(ctx));
74
+ }
75
+ return diagnostics;
76
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "rules": {
3
+ "COR001": "warning",
4
+ "COR002": "off",
5
+ "COR003": "off",
6
+ "COR004": "off",
7
+ "COR005": "off",
8
+ "COR006": "off",
9
+ "COR007": "off",
10
+ "COR008": "warning",
11
+ "COR009": "off",
12
+ "COR010": "warning",
13
+ "COR011": "warning",
14
+ "COR012": "off",
15
+ "COR013": "off",
16
+ "COR014": "off",
17
+ "COR015": "off"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "rules": {
3
+ "COR001": "error",
4
+ "COR002": "error",
5
+ "COR003": "warning",
6
+ "COR004": "warning",
7
+ "COR005": "warning",
8
+ "COR006": "error",
9
+ "COR007": "warning",
10
+ "COR008": "error",
11
+ "COR009": "warning",
12
+ "COR010": "warning",
13
+ "COR011": "error",
14
+ "COR012": "warning",
15
+ "COR013": "info",
16
+ "COR014": "warning",
17
+ "COR015": "warning"
18
+ }
19
+ }