@rcrsr/rill-cli 0.6.0

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Tests for CLI error explanation functionality
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { explainError } from '../src/cli-explain.js';
7
+
8
+ describe('explainError', () => {
9
+ describe('valid error IDs', () => {
10
+ it('returns formatted documentation for error with all fields', () => {
11
+ // Create a mock error with all optional fields populated
12
+ // We need to find an error ID that has cause, resolution, and examples
13
+ // For this test, we'll use RILL-R009 if it has these fields, otherwise we need to check
14
+
15
+ const result = explainError('RILL-R009');
16
+
17
+ // Since RILL-R009 exists in ERROR_REGISTRY, this should not be null
18
+ expect(result).not.toBeNull();
19
+
20
+ if (result !== null) {
21
+ // Should contain the error ID and description
22
+ expect(result).toContain('RILL-R009');
23
+ expect(result).toContain('Property not found');
24
+ }
25
+ });
26
+
27
+ it('returns formatted documentation for lexer error', () => {
28
+ const result = explainError('RILL-L001');
29
+
30
+ expect(result).not.toBeNull();
31
+ if (result !== null) {
32
+ expect(result).toContain('RILL-L001');
33
+ expect(result).toContain('Unterminated string literal');
34
+ }
35
+ });
36
+
37
+ it('returns formatted documentation for parse error', () => {
38
+ const result = explainError('RILL-P001');
39
+
40
+ expect(result).not.toBeNull();
41
+ if (result !== null) {
42
+ expect(result).toContain('RILL-P001');
43
+ expect(result).toContain('Unexpected token');
44
+ }
45
+ });
46
+
47
+ it('returns formatted documentation for runtime error', () => {
48
+ const result = explainError('RILL-R001');
49
+
50
+ expect(result).not.toBeNull();
51
+ if (result !== null) {
52
+ expect(result).toContain('RILL-R001');
53
+ expect(result).toContain('Parameter type mismatch');
54
+ }
55
+ });
56
+
57
+ it('returns formatted documentation for check error', () => {
58
+ const result = explainError('RILL-C001');
59
+
60
+ expect(result).not.toBeNull();
61
+ if (result !== null) {
62
+ expect(result).toContain('RILL-C001');
63
+ expect(result).toContain('File not found');
64
+ }
65
+ });
66
+
67
+ it('includes cause section when present', () => {
68
+ // We need to check which error has cause field populated
69
+ // For now, we'll test the structure by creating a scenario
70
+ const result = explainError('RILL-R001');
71
+
72
+ if (result !== null && result.includes('Cause:')) {
73
+ expect(result).toMatch(/Cause:\n {2}.+/);
74
+ }
75
+ });
76
+
77
+ it('includes resolution section when present', () => {
78
+ const result = explainError('RILL-R001');
79
+
80
+ if (result !== null && result.includes('Resolution:')) {
81
+ expect(result).toMatch(/Resolution:\n {2}.+/);
82
+ }
83
+ });
84
+
85
+ it('includes examples section when present', () => {
86
+ const result = explainError('RILL-R001');
87
+
88
+ if (result !== null && result.includes('Examples:')) {
89
+ expect(result).toContain('Examples:');
90
+ // Should have indented code
91
+ expect(result).toMatch(/ {4}.+/);
92
+ }
93
+ });
94
+
95
+ it('formats output with proper structure', () => {
96
+ const result = explainError('RILL-R001');
97
+
98
+ expect(result).not.toBeNull();
99
+ if (result !== null) {
100
+ // Should start with errorId: description
101
+ expect(result).toMatch(/^RILL-R\d{3}: .+/);
102
+
103
+ // Should not have trailing whitespace
104
+ expect(result).not.toMatch(/\s$/);
105
+ }
106
+ });
107
+ });
108
+
109
+ describe('invalid error IDs', () => {
110
+ it('returns null for invalid errorId format - missing prefix', () => {
111
+ const result = explainError('R001');
112
+ expect(result).toBeNull();
113
+ });
114
+
115
+ it('returns null for invalid errorId format - wrong prefix', () => {
116
+ const result = explainError('RILL001');
117
+ expect(result).toBeNull();
118
+ });
119
+
120
+ it('returns null for invalid errorId format - invalid category', () => {
121
+ const result = explainError('RILL-X001');
122
+ expect(result).toBeNull();
123
+ });
124
+
125
+ it('returns null for invalid errorId format - too few digits', () => {
126
+ const result = explainError('RILL-R01');
127
+ expect(result).toBeNull();
128
+ });
129
+
130
+ it('returns null for invalid errorId format - too many digits', () => {
131
+ const result = explainError('RILL-R0001');
132
+ expect(result).toBeNull();
133
+ });
134
+
135
+ it('returns null for invalid errorId format - lowercase', () => {
136
+ const result = explainError('rill-r001');
137
+ expect(result).toBeNull();
138
+ });
139
+
140
+ it('returns null for invalid errorId format - empty string', () => {
141
+ const result = explainError('');
142
+ expect(result).toBeNull();
143
+ });
144
+
145
+ it('returns null for invalid errorId format - random string', () => {
146
+ const result = explainError('invalid');
147
+ expect(result).toBeNull();
148
+ });
149
+ });
150
+
151
+ describe('unknown error IDs', () => {
152
+ it('returns null for unknown lexer error ID', () => {
153
+ const result = explainError('RILL-L999');
154
+ expect(result).toBeNull();
155
+ });
156
+
157
+ it('returns null for unknown parse error ID', () => {
158
+ const result = explainError('RILL-P999');
159
+ expect(result).toBeNull();
160
+ });
161
+
162
+ it('returns null for unknown runtime error ID', () => {
163
+ const result = explainError('RILL-R999');
164
+ expect(result).toBeNull();
165
+ });
166
+
167
+ it('returns null for unknown check error ID', () => {
168
+ const result = explainError('RILL-C999');
169
+ expect(result).toBeNull();
170
+ });
171
+ });
172
+
173
+ describe('output formatting', () => {
174
+ it('indents code examples with 4 spaces', () => {
175
+ // Find an error with examples and test indentation
176
+ const result = explainError('RILL-R001');
177
+
178
+ if (result !== null && result.includes('Examples:')) {
179
+ const lines = result.split('\n');
180
+ const codeLines = lines.filter((line) => line.match(/^ {4}[^ ]/));
181
+ // If examples exist, should have at least one code line
182
+ if (lines.some((line) => line.includes('Examples:'))) {
183
+ // Code lines should be indented with exactly 4 spaces
184
+ for (const line of codeLines) {
185
+ expect(line).toMatch(/^ {4}/);
186
+ }
187
+ }
188
+ }
189
+ });
190
+
191
+ it('indents example descriptions with 2 spaces', () => {
192
+ const result = explainError('RILL-R001');
193
+
194
+ if (result !== null && result.includes('Examples:')) {
195
+ const lines = result.split('\n');
196
+ // Find lines after "Examples:" that are descriptions (before code)
197
+ let inExamples = false;
198
+ for (const line of lines) {
199
+ if (line === 'Examples:') {
200
+ inExamples = true;
201
+ continue;
202
+ }
203
+ if (
204
+ inExamples &&
205
+ line.trim() !== '' &&
206
+ !line.startsWith(' ') &&
207
+ line.startsWith(' ')
208
+ ) {
209
+ // This is a description line
210
+ expect(line).toMatch(/^ {2}[^ ]/);
211
+ }
212
+ }
213
+ }
214
+ });
215
+
216
+ it('separates sections with blank lines', () => {
217
+ const result = explainError('RILL-R001');
218
+
219
+ if (result !== null) {
220
+ // Should have blank lines between sections
221
+ const lines = result.split('\n');
222
+
223
+ // Find section headers and check they're followed by content then blank line
224
+ const sectionHeaders = ['Cause:', 'Resolution:', 'Examples:'];
225
+ for (const header of sectionHeaders) {
226
+ const headerIndex = lines.indexOf(header);
227
+ if (headerIndex !== -1 && headerIndex < lines.length - 2) {
228
+ // After the header content, should have a blank line
229
+ // (unless it's the last section)
230
+ let nextNonContentIndex = headerIndex + 1;
231
+ while (
232
+ nextNonContentIndex < lines.length &&
233
+ lines[nextNonContentIndex]?.trim() !== '' &&
234
+ !sectionHeaders.includes(lines[nextNonContentIndex] ?? '')
235
+ ) {
236
+ nextNonContentIndex++;
237
+ }
238
+ if (
239
+ nextNonContentIndex < lines.length &&
240
+ !sectionHeaders.includes(lines[nextNonContentIndex] ?? '')
241
+ ) {
242
+ expect(lines[nextNonContentIndex]).toBe('');
243
+ }
244
+ }
245
+ }
246
+ }
247
+ });
248
+ });
249
+ });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * CLI LSP Diagnostic Tests
3
+ * Test LSP diagnostic conversion from RillError
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { toLspDiagnostic } from '../src/cli-lsp-diagnostic.js';
8
+ import { RillError, RuntimeError } from '@rcrsr/rill';
9
+
10
+ describe('CLI LSP Diagnostic', () => {
11
+ describe('toLspDiagnostic', () => {
12
+ // IR-9: Convert RillError with span to LspDiagnostic with zero-based positions
13
+ it('converts RillError with location to LspDiagnostic', () => {
14
+ const error = new RuntimeError(
15
+ 'RILL-R005',
16
+ 'Variable foo is not defined',
17
+ { line: 5, column: 10, offset: 42 },
18
+ { name: 'foo' }
19
+ );
20
+
21
+ const diagnostic = toLspDiagnostic(error);
22
+
23
+ expect(diagnostic).toEqual({
24
+ range: {
25
+ start: { line: 4, character: 9 }, // 1-based line 5, column 10 -> 0-based line 4, character 9
26
+ end: { line: 4, character: 9 },
27
+ },
28
+ severity: 1, // Error
29
+ code: 'RILL-R005',
30
+ source: 'rill',
31
+ message: 'Variable foo is not defined',
32
+ });
33
+ });
34
+
35
+ // EC-11: Missing span returns diagnostic with null range
36
+ it('converts RillError without location to diagnostic with null range', () => {
37
+ const error = new RuntimeError(
38
+ 'RILL-R010',
39
+ 'Iteration limit of 1000 exceeded',
40
+ undefined,
41
+ { limit: 1000 }
42
+ );
43
+
44
+ const diagnostic = toLspDiagnostic(error);
45
+
46
+ expect(diagnostic).toEqual({
47
+ range: null,
48
+ severity: 1, // Error
49
+ code: 'RILL-R010',
50
+ source: 'rill',
51
+ message: 'Iteration limit of 1000 exceeded',
52
+ });
53
+ });
54
+
55
+ it('converts zero-based positions correctly', () => {
56
+ const error = new RuntimeError(
57
+ 'RILL-R005',
58
+ 'Variable x is not defined',
59
+ { line: 1, column: 1, offset: 0 }, // First line, first column
60
+ { name: 'x' }
61
+ );
62
+
63
+ const diagnostic = toLspDiagnostic(error);
64
+
65
+ expect(diagnostic.range).toEqual({
66
+ start: { line: 0, character: 0 }, // 1-based line 1, column 1 -> 0-based line 0, character 0
67
+ end: { line: 0, character: 0 },
68
+ });
69
+ });
70
+
71
+ it('strips location suffix from message', () => {
72
+ const error = new RuntimeError(
73
+ 'RILL-R005',
74
+ 'Variable foo is not defined',
75
+ { line: 10, column: 5, offset: 100 }
76
+ );
77
+
78
+ const diagnostic = toLspDiagnostic(error);
79
+
80
+ // Message should not include " at 10:5" suffix
81
+ expect(diagnostic.message).toBe('Variable foo is not defined');
82
+ expect(diagnostic.message).not.toContain(' at 10:5');
83
+ });
84
+
85
+ it('always sets source to rill', () => {
86
+ const error = new RuntimeError('RILL-R001', 'Type error', {
87
+ line: 1,
88
+ column: 1,
89
+ offset: 0,
90
+ });
91
+
92
+ const diagnostic = toLspDiagnostic(error);
93
+
94
+ expect(diagnostic.source).toBe('rill');
95
+ });
96
+
97
+ it('maps error severity to LSP severity 1', () => {
98
+ const error = new RuntimeError(
99
+ 'RILL-R005',
100
+ 'Variable foo is not defined',
101
+ { line: 1, column: 1, offset: 0 }
102
+ );
103
+
104
+ const diagnostic = toLspDiagnostic(error);
105
+
106
+ expect(diagnostic.severity).toBe(1); // Error
107
+ });
108
+
109
+ it('includes suggestions when present in context', () => {
110
+ const error = new RillError({
111
+ errorId: 'RILL-R005',
112
+ message: 'Variable foo is not defined',
113
+ location: { line: 1, column: 1, offset: 0 },
114
+ context: {
115
+ name: 'foo',
116
+ suggestions: ['foobar', 'food', 'foot'],
117
+ },
118
+ });
119
+
120
+ const diagnostic = toLspDiagnostic(error);
121
+
122
+ expect(diagnostic.suggestions).toEqual(['foobar', 'food', 'foot']);
123
+ });
124
+
125
+ it('limits suggestions to max 3 entries', () => {
126
+ const error = new RillError({
127
+ errorId: 'RILL-R005',
128
+ message: 'Variable foo is not defined',
129
+ location: { line: 1, column: 1, offset: 0 },
130
+ context: {
131
+ name: 'foo',
132
+ suggestions: ['a', 'b', 'c', 'd', 'e'],
133
+ },
134
+ });
135
+
136
+ const diagnostic = toLspDiagnostic(error);
137
+
138
+ expect(diagnostic.suggestions).toEqual(['a', 'b', 'c']);
139
+ expect(diagnostic.suggestions?.length).toBe(3);
140
+ });
141
+
142
+ it('omits suggestions field when not present', () => {
143
+ const error = new RuntimeError(
144
+ 'RILL-R005',
145
+ 'Variable foo is not defined',
146
+ { line: 1, column: 1, offset: 0 },
147
+ { name: 'foo' } // No suggestions in context
148
+ );
149
+
150
+ const diagnostic = toLspDiagnostic(error);
151
+
152
+ expect(diagnostic).not.toHaveProperty('suggestions');
153
+ });
154
+
155
+ it('omits suggestions field when empty array', () => {
156
+ const error = new RillError({
157
+ errorId: 'RILL-R005',
158
+ message: 'Variable foo is not defined',
159
+ location: { line: 1, column: 1, offset: 0 },
160
+ context: {
161
+ name: 'foo',
162
+ suggestions: [],
163
+ },
164
+ });
165
+
166
+ const diagnostic = toLspDiagnostic(error);
167
+
168
+ expect(diagnostic).not.toHaveProperty('suggestions');
169
+ });
170
+
171
+ it('filters out empty suggestion strings', () => {
172
+ const error = new RillError({
173
+ errorId: 'RILL-R005',
174
+ message: 'Variable foo is not defined',
175
+ location: { line: 1, column: 1, offset: 0 },
176
+ context: {
177
+ name: 'foo',
178
+ suggestions: ['valid', '', 'also-valid'],
179
+ },
180
+ });
181
+
182
+ const diagnostic = toLspDiagnostic(error);
183
+
184
+ expect(diagnostic.suggestions).toEqual(['valid', 'also-valid']);
185
+ });
186
+
187
+ it('handles multi-line spans by using start location', () => {
188
+ const error = new RuntimeError(
189
+ 'RILL-R002',
190
+ 'Operator type mismatch',
191
+ { line: 5, column: 10, offset: 42 } // Only start location available
192
+ );
193
+
194
+ const diagnostic = toLspDiagnostic(error);
195
+
196
+ expect(diagnostic.range).toEqual({
197
+ start: { line: 4, character: 9 },
198
+ end: { line: 4, character: 9 },
199
+ });
200
+ });
201
+ });
202
+ });